From e9e1111df90d2e36a3c38cf3b0c2fd1774ec3925 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sat, 13 Dec 2025 08:05:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20NBA=20=E9=87=8F?= =?UTF-8?q?=E5=8C=96=E4=BA=A4=E6=98=93=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端实现: - 实现 NBA 比赛数据服务,从 Polymarket API 获取数据 - 实现数据库存储和增量拉取逻辑(优先从 DB 获取,数据不足时增量拉取) - 使用 sports_market_types 参数直接筛选 moneyline 类型 - 实现分页拉取逻辑(基于 gameStartTime 和 createdAt) - 移除 nba_markets 相关的外键约束(V12 迁移) - 修复数据拉取逻辑:超过 3 天的数据不拉取 - 前端实现: - 实现策略创建/编辑/列表页面 - 实现交易信号展示页面和统计页面 - 修复重复请求问题(使用 useCallback 包装 fetchGames) - 支持选择单场比赛进行配置 - 使用西8区时间格式化显示 - 数据库: - 创建 NBA 量化交易相关表(V11 迁移) - 移除外键约束(V12 迁移) - 文档: - 添加产品需求文档、技术方案、算法文档等 --- NBA.md | 1028 +++++++++++++++++ .../wrbug/polymarketbot/api/NbaStatsApi.kt | 102 ++ .../polymarketbot/api/PolymarketGammaApi.kt | 55 +- .../controller/nba/NbaGameController.kt | 69 ++ .../controller/nba/NbaMarketController.kt | 70 ++ .../nba/NbaQuantitativeStrategyController.kt | 166 +++ .../com/wrbug/polymarketbot/dto/NbaGameDto.kt | 40 + .../wrbug/polymarketbot/dto/NbaMarketDto.kt | 45 + .../dto/NbaQuantitativeStrategyDto.kt | 213 ++++ .../com/wrbug/polymarketbot/entity/NbaGame.kt | 55 + .../wrbug/polymarketbot/entity/NbaMarket.kt | 63 + .../entity/NbaQuantitativeStrategy.kt | 196 ++++ .../entity/NbaStrategyStatistics.kt | 50 + .../polymarketbot/entity/NbaTradingSignal.kt | 64 + .../wrbug/polymarketbot/enums/SportsTagId.kt | 66 ++ .../repository/NbaGameRepository.kt | 27 + .../repository/NbaMarketRepository.kt | 15 + .../NbaQuantitativeStrategyRepository.kt | 14 + .../NbaStrategyStatisticsRepository.kt | 14 + .../repository/NbaTradingSignalRepository.kt | 18 + .../service/nba/NbaGameService.kt | 579 ++++++++++ .../service/nba/NbaMarketNameParser.kt | 314 +++++ .../service/nba/NbaMarketService.kt | 165 +++ .../nba/NbaQuantitativeStrategyService.kt | 346 ++++++ .../com/wrbug/polymarketbot/util/JsonUtils.kt | 24 + .../polymarketbot/util/NbaApiValidator.kt | 64 + .../polymarketbot/util/RetrofitFactory.kt | 39 + ...create_nba_quantitative_trading_tables.sql | 184 +++ .../V12__remove_nba_markets_foreign_keys.sql | 37 + .../service/nba/NbaGameServiceTest.kt | 59 + .../nba-api-integration-technical-solution.md | 999 ++++++++++++++++ docs/zh/nba-api-verification-checklist.md | 158 +++ docs/zh/nba-backend-implementation-summary.md | 218 ++++ docs/zh/nba-game-data-sources-comparison.md | 100 ++ .../nba-hybrid-data-source-implementation.md | 150 +++ .../zh/nba-quantitative-strategy-algorithm.md | 834 +++++++++++++ ...antitative-trading-product-requirements.md | 732 ++++++++++++ .../nba-strategy-configurable-parameters.md | 567 +++++++++ ...olymarket-nba-markets-fetching-solution.md | 886 ++++++++++++++ frontend/src/App.tsx | 10 + frontend/src/components/Layout.tsx | 29 +- frontend/src/locales/zh-CN/common.json | 5 +- .../src/pages/NbaQuantitativeStrategyAdd.tsx | 828 +++++++++++++ .../src/pages/NbaQuantitativeStrategyEdit.tsx | 439 +++++++ .../src/pages/NbaQuantitativeStrategyList.tsx | 227 ++++ frontend/src/pages/NbaStatistics.tsx | 231 ++++ frontend/src/pages/NbaTradingSignals.tsx | 282 +++++ frontend/src/services/api.ts | 114 +- frontend/src/types/index.ts | 290 +++++ landing-page | 1 + 50 files changed, 11274 insertions(+), 7 deletions(-) create mode 100644 NBA.md create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/api/NbaStatsApi.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaGameController.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaMarketController.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaQuantitativeStrategyController.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaGameDto.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaMarketDto.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaQuantitativeStrategyDto.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaGame.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaMarket.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaQuantitativeStrategy.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaStrategyStatistics.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaTradingSignal.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/enums/SportsTagId.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaGameRepository.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaMarketRepository.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaQuantitativeStrategyRepository.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaStrategyStatisticsRepository.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaTradingSignalRepository.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameService.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketNameParser.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketService.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaQuantitativeStrategyService.kt create mode 100644 backend/src/main/kotlin/com/wrbug/polymarketbot/util/NbaApiValidator.kt create mode 100644 backend/src/main/resources/db/migration/V11__create_nba_quantitative_trading_tables.sql create mode 100644 backend/src/main/resources/db/migration/V12__remove_nba_markets_foreign_keys.sql create mode 100644 backend/src/test/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameServiceTest.kt create mode 100644 docs/zh/nba-api-integration-technical-solution.md create mode 100644 docs/zh/nba-api-verification-checklist.md create mode 100644 docs/zh/nba-backend-implementation-summary.md create mode 100644 docs/zh/nba-game-data-sources-comparison.md create mode 100644 docs/zh/nba-hybrid-data-source-implementation.md create mode 100644 docs/zh/nba-quantitative-strategy-algorithm.md create mode 100644 docs/zh/nba-quantitative-trading-product-requirements.md create mode 100644 docs/zh/nba-strategy-configurable-parameters.md create mode 100644 docs/zh/polymarket-nba-markets-fetching-solution.md create mode 100644 frontend/src/pages/NbaQuantitativeStrategyAdd.tsx create mode 100644 frontend/src/pages/NbaQuantitativeStrategyEdit.tsx create mode 100644 frontend/src/pages/NbaQuantitativeStrategyList.tsx create mode 100644 frontend/src/pages/NbaStatistics.tsx create mode 100644 frontend/src/pages/NbaTradingSignals.tsx create mode 160000 landing-page diff --git a/NBA.md b/NBA.md new file mode 100644 index 0000000..ec26017 --- /dev/null +++ b/NBA.md @@ -0,0 +1,1028 @@ +# NBA 比赛胜负分析框架 + +## 一、 赛前预测核心要点 + +预测比赛结束时的结果。 + +### 核心变量 (权重最高) +1. **核心球星状态**: 球队头号球星的近期数据、健康状况、是否有轮休可能。 + - **量化指标**: + - 近期 5 场平均 PER (Player Efficiency Rating) + - 近期 5 场真实命中率 (TS%) 与赛季平均的差值 + - 近期 5 场使用率 (USG%) 变化 + - 伤病报告状态 (健康/出战成疑/缺席) + - 轮休概率 (基于赛程密度和历史轮休模式) +2. **关键阵容完整度**: 除球星外,第二当家、重要首发或第六人是否缺席。 + - **量化指标**: + - 缺失球员的 VORP (Value Over Replacement Player) + - 缺失球员的 BPM (Box Plus/Minus) + - 阵容完整度评分 = `1 - (缺失球员总VORP / 球队总VORP)` + - 替补深度评分 (基于替补球员的平均 PER) +3. **球队近期势头**: 双方近 5-10 场的战绩,是否处于连胜或连败中。 + - **量化指标**: + - 近 5 场/10 场胜率 + - 连胜/连败场次 + - 近期净效率值变化趋势 (与赛季平均对比) + - 近期对手平均实力 (SRS - Simple Rating System) + +### 团队实力与风格维度 (基本盘和打法) + +#### 攻防效率定位 +1. 进攻效率 (每百回合得分) 联盟排名 +2. 防守效率 (每百回合失分) 联盟排名 +3. **净效率值 (攻防效率差)**: 真实战力核心指标 + +#### 比赛风格特征 +1. **节奏**: 喜快还是喜慢 +2. **生命线**: 球队极度依赖的赢球方式 +3. **关键数据**: 篮板 (前场板)、失误控制、内线得分、三分产出与效率 + +### 对位与相克关系 (突破口) +1. **球星对位**: 一方是否有球员能在防守端有效限制对方头牌。 + - **量化指标**: + - 历史对位数据: 防守球员对位进攻球员时的限制效果 + - 对位效率差 = `(对位时进攻球员TS% - 赛季平均TS%)` + - 对位得分差 = `(对位时平均得分 - 赛季平均得分)` + - 防守球员的 DRPM (Defensive Real Plus-Minus) +2. **阵容克制**: 内线强队 VS 外线投射型球队;防守强队 VS 进攻单一球队。 + - **量化指标**: + - 内线得分占比 vs 对手内线防守效率 + - 三分出手占比 vs 对手三分防守效率 + - 快攻得分占比 vs 对手快攻防守效率 + - 风格匹配度评分 (基于历史对战数据) +3. **风格喜好**: 对方是否恰好擅长打击我方的弱点。 + - **量化指标**: + - 对手进攻效率 vs 我方防守效率 (按进攻类型分类) + - 弱点暴露度 = `(对手擅长进攻方式得分占比) × (我方该方式防守效率排名)` + +### 赛程与外部因素 +1. **主客场与旅行**: 球队是主场以逸待劳,还是历经长途客场之旅的最后一站。 + - **量化指标**: + - 主客场胜率差 (主场胜率 - 客场胜率) + - 旅行距离 (英里) 和时区变化 + - 连续客场场次 + - 休息天数 (距离上一场比赛的天数) +2. **背靠背作战**: 是否有球队是背靠背第二场?球员体能会受影响。 + - **量化指标**: + - 背靠背第二场历史胜率 (vs 正常比赛胜率) + - 背靠背第二场平均净效率值变化 + - 体能影响系数 = `-0.05 × (背靠背标识)` +3. **赛程难度**: 球队近期的对手是强是弱?战绩是否存在"水分"。 + - **量化指标**: + - 近期对手平均 SRS (Simple Rating System) + - 近期对手平均净效率值 + - 赛程强度排名 (联盟排名) + - 战绩"水分"系数 = `(实际胜率 - 预期胜率) / 预期胜率` +4. **战意与特殊情节**: 是否是季后赛卡位关键战、恩怨对决或复仇之战。 + - **量化指标**: + - 季后赛卡位关键度 (基于当前排名和剩余场次) + - 历史恩怨得分 (基于近期对战记录和冲突事件) + - 复仇动机 (上次对战是否失利) + +### 其他因素 +- **“临场”与“偶然性”**: 突发伤病、临场手感、裁判尺度、关键球的偶然性。 + +> 批注[§★威★§1]: 该部分内容需要结合球队历史数据及球队详细分析数据,量化到个人。 + +--- + +## 二、 赛中实时预测:剩余比赛进程走向 + +核心公式:`剩余时间预期净胜分 = f(当前分差, 剩余时间, 球队实力, 比赛状态, 环境因素...)` + +### 实时比赛状态 +1. **核心变量**: 当前分差、剩余比赛时间 (精确到秒)、当前节次、球权归属。 +2. **关键数据**: 分差/剩余时间比 (即“每分钟需要追几分”)、是否有加时。 + +### 球队真实能力 (赛季平均值,可动态调整) +1. **攻防效率**: 球队每百回合净胜分。 +2. **比赛节奏**: 球队平均每 48 分钟回合数 (用于估算剩余进攻回合数)。 +3. **风格特质**: + 1) **三分能力**: 擅长三分的球队有短时间内爆发追分的能力。 + 2) **造罚球能力**: 能稳定造犯规的球队追分方式更稳健。 + 3) **关键球表现**: 球队在最后 5 分钟、分差 5 分以内的攻防效率。 + +### 球员与阵容在场状态 (动态权重) +1. **球星在场情况**: 追分阶段,球队核心是否在场上。 +2. **阵容攻防属性**: 当前场上阵容是偏进攻还是偏防守。 +3. **球员体能/犯规**: 核心球员的犯规次数、已上场时间。 +4. **手感热度**: 可采用滚动窗口统计数据 (近 10 个回合的命中率)。 + +### 比赛环境与动量 (心理与局势因子) +1. **主客场**: 主场球队通常有更高的“裁判哨倾向”和气势加成。 +2. **势头**: 可量化为过去几个回合的净胜分。 +3. **暂停与挑战**: 剩余的暂停次数。 + +> 批注[§★威★§2]: 该部分内容需结合历史数据及临场数据,量化到个人。 + +--- + +## 三、 预测模型构建的三个阶段 + +### 阶段一:基线模型 - 基于历史概率的“速查表” + +> 批注[§★威★§3]: 速查表作为当前情况的历史依据,只能作参照。 + +1. **数据准备**: 收集大量历史比赛数据,切割出每一个 `(分差, 剩余时间)` 的组合瞬间 (如: 落后 10 分,剩余 5 分钟)。 +2. **概率统计**: 统计在所有历史同类瞬间中,最终翻盘获胜的比例,以及最终分差的平均值/分布。 +3. **应用**: 查询当前情况所属的历史瞬间组,直接得出历史翻盘概率和平均追回分差。 + +### 阶段二:进阶模型 - 基于球队能力调整的预测 + +> 批注[§★威★§4]: 结合球队真实数据对预测进行校正。 + +在基线模型上,引入球队实力因子进行校正。 + +1. **计算公式 (简化示例)** + `预期追回分差 = 基线模型平均追回分差 × (实力系数A / 实力系数B)` +2. **实力系数**: 可采用两队赛季平均的百回合净胜分差。`实力系数 = 球队净效率值 - 联盟平均值` +3. **应用示例**: 假设基线模型显示落后 10 分 (剩余 5 分钟) 平均能追回 6 分。若追分方净效率强于对手 10%,则调整后的预期追回分差 = 6 × 1.1 = 6.6 分。 + +### 阶段三:高级模型 - 机器学习动态预测 + +> 批注[§★威★§5]: 动态数据实时分析更新预测。 + +1. **模型选择**: 适合的算法包括梯度提升决策树 (XGBoost, LightGBM) 或随机森林。 +2. **特征工程**: 综合上述所有变量,构建数百个特征。 + - **时间特征** (5-10 个): + - 剩余时间 (秒、分钟) + - 当前节次 (1-4, 加时) + - 是否加时 + - 剩余暂停次数 (主队、客队) + - 是否最后 5 分钟 + - 是否最后 2 分钟 + - 是否最后 1 分钟 + - **分差特征** (10-15 个): + - 当前分差 (绝对值、带符号) + - 分差变化率 (过去 N 回合) + - 最大分差 (本场比赛) + - 分差/剩余时间比 + - 分差是否在 5 分以内 + - 分差是否在 10 分以内 + - 分差趋势 (扩大/缩小/持平) + - **球队基础特征** (20-30 个): + - 球队净效率值 (赛季平均、近期平均) + - 进攻效率排名 + - 防守效率排名 + - 比赛节奏 (Pace) + - 三分出手占比 + - 三分命中率 + - 罚球率 (FTA/FGA) + - 前场篮板率 + - 失误率 + - 关键时刻 (Clutch) 攻防效率 + - **球员在场特征** (30-50 个): + - 在场球员总 PER + - 在场球员平均真实命中率 + - 核心球星是否在场 (0/1) + - 核心球星本场 TS% + - 核心球星本场使用率 + - 核心球星犯规次数 + - 核心球星已上场时间 + - 阵容 Net Rating (当前阵容) + - 阵容高度 (平均身高) + - 阵容经验 (平均年龄或球龄) + - **实时状态特征** (20-30 个): + - 过去 5/10/15 回合净胜分 + - 过去 N 回合命中率 + - 过去 N 回合三分命中率 + - 过去 N 回合失误数 + - 势头评分 (基于近期净胜分) + - 手感热度 (滚动窗口命中率) + - **环境特征** (10-15 个): + - 主客场 (0/1) + - 休息天数 + - 是否背靠背 + - 旅行距离 + - 时区变化 + - 赛程强度 (近期对手平均实力) + - **历史对位特征** (15-20 个): + - 本赛季对战记录 + - 历史对战净效率值 + - 对位限制效果 (核心球员对位数据) + - 风格匹配度 + - **交互特征** (50-100 个): + - 分差 × 剩余时间 + - 球队实力差 × 剩余时间 + - 势头 × 剩余时间 + - 各种特征之间的乘积和比值 + - **特征总数**: 建议 150-300 个特征,通过特征选择筛选出最重要的 50-100 个 +3. **目标变量**: + - **分类任务**: 预测本场比赛最终是否翻盘 (是/否)。 + - **回归任务**: 预测比赛结束时的最终分差。 + - **最佳实践**: 两个任务同时进行,用分类概率评估翻盘可能性,用回归值评估预期追回分数。 +4. **实时预测**: 模型在比赛中每隔一个回合 (或每分钟) 重新运行一次,输入最新的实时数据,输出更新的预测概率和分差。 + +--- + +## 四、 模块化预测框架详解 + +核心:计算追分方在剩余时间内**预期能比对手多得的分数**(预期追回分差 E)。 + +**核心公式**: +`预期追回分差 (E) = (基础追分潜力 × W1 + 实时状态系数 × W2) × 时间调整系数 + 随机波动项` + +**公式说明**: +- 随机波动项 ε 应该在时间调整之后添加,因为随机性影响的是最终的分差结果,而不是追分潜力本身 +- 另一种可选形式: `E = [(F-base × W1 + F-live × W2) × f(T)] + ε`,其中 ε 在每次蒙特卡洛模拟中独立生成 + +> 批注[§★威★§7]: 核心: 在剩余时间内预期追回分差。 + +### 第一步:定义与计算输入模块 + +#### 1. 基础追分潜力 (F-base) +- **球队实力系数 (S)**: 使用两队赛季平均的净效率值差。 + - **计算公式**: `S = (追分方净效率值 - 对手净效率值) / 归一化因子` + - **归一化方法**: + - 方法一 (Z-score): `S = (净效率差) / (联盟净效率标准差)`,通常范围 [-2, 2] + - 方法二 (Min-Max): `S = (净效率差 - 最小差) / (最大差 - 最小差) × 2 - 1`,范围 [-1, 1] + - 方法三 (简单缩放): `S = 净效率差 / 10`,例如净效率差 3.0 → S = 0.3 + - **示例**: 追分方净效率 +5.0,对手 +2.0,净效率差 = 3.0 + - Z-score 法: 假设联盟标准差 = 5.0,则 `S = 3.0 / 5.0 = 0.6` + - 简单缩放: `S = 3.0 / 10 = 0.3` +- **阵容对位系数 (L)**: 评估当前在场阵容的优劣势。 + - **计算公式**: `L = (追分方阵容 Net Rating - 对手阵容 Net Rating) / 归一化因子` + - **归一化方法**: 同实力系数 S,使用 Z-score 或 Min-Max 归一化 + - **数据来源**: + - 当前阵容本赛季的 Net Rating (每百回合净胜分) + - 如果阵容数据不足,使用球队平均 Net Rating × 阵容调整系数 + - **示例**: 追分方阵容 Net Rating = +8.0,对手 = +5.0,差值为 3.0 + - 归一化后: `L = 3.0 / 10 = 0.3` (使用简单缩放) +- **F-base 小计**: `F-base = S + L` + - **注意**: S 和 L 在相加前必须确保使用相同的归一化方法,且量级相近 + - **推荐做法**: 两者都使用 Z-score 归一化,或都使用 Min-Max 归一化到 [-1, 1] 范围 + - **权重调整** (可选): 如果 S 和 L 的重要性不同,可以使用加权和: `F-base = α × S + (1-α) × L`,其中 α 通常取 0.6-0.7 + +#### 2. 实时状态系数 (F-live) +- **势头动量 (M)**: 计算过去 N 个回合 (如 10 回合) 的净胜分,并归一化。 + - **计算公式**: `M = (过去N回合净胜分) / (N × 平均单回合得分)` + - **归一化方法**: + - 方法一: `M = (净胜分) / (N × 2.0)`,假设平均单回合得分约 2.0 分 + - 方法二: 使用历史数据统计,计算过去 N 回合净胜分的 Z-score + - 方法三: `M = tanh(净胜分 / (N × 1.5))`,使用双曲正切函数将值限制在 [-1, 1] + - **示例**: 过去 10 回合净胜 +8 分 + - 方法一: `M = 8 / (10 × 2.0) = 0.4` + - 方法三: `M = tanh(8 / 15) = tanh(0.533) ≈ 0.49` + - **动态窗口**: N 值可根据比赛阶段调整 + - 比赛早期: N = 15-20 回合 + - 比赛后期: N = 8-10 回合 + - 关键时刻: N = 5-8 回合 +- **球星爆发因子 (P)**: 核心球星在本场比赛的真实命中率 (TS%) 是否远高于赛季平均值。 + - **计算公式**: `P = (本场TS% - 赛季平均TS%) / 100` + - **归一化方法**: + - 直接使用百分比差值,范围通常在 [-0.3, 0.3] + - 使用 tanh 函数限制: `P = tanh((TS%差值) / 15)` + - **示例**: 本场 TS% = 65%,赛季平均 = 55%,差值为 10 个百分点 + - 直接法: `P = 10 / 100 = 0.1` + - tanh 法: `P = tanh(10 / 15) = tanh(0.667) ≈ 0.58` + - **多球星情况**: 如果有多个核心球星,取加权平均 + - `P = Σ(球员P值 × 球员使用率权重)` +- **F-live 小计**: `F-live = M + P` + +#### 3. 随机波动项 (ε) +- 符合特定分布 (如均值为 0,方差可调的正态分布) 的随机数,用于模拟无法预测的偶发事件 (争议判罚、意外失误、神奇进球)。每次模拟时随机生成。 +- **分布选择**: + - **正态分布**: `ε ~ N(0, σ²)`,其中 σ 为方差 + - **t 分布**: 适用于尾部更厚的分布 (极端事件更多) + - **经验分布**: 基于历史数据的实际分差波动分布 +- **方差 (σ²) 的确定**: + - **方法一**: 基于历史数据统计 + - 收集所有历史比赛在相同 `(分差, 剩余时间)` 条件下的实际追回分差 + - 计算实际追回分差与模型预测值的残差 + - 残差的标准差即为 σ + - **方法二**: 分阶段设置 + - 比赛早期: σ = 2.0 (不确定性较大) + - 比赛中期: σ = 1.5 + - 比赛后期: σ = 1.0 (时间越少,随机性影响越小) + - 关键时刻: σ = 0.8 + - **方法三**: 基于分差动态调整 + - 大分差: σ 较大 (追分难度大,波动也大) + - 小分差: σ 较小 (接近的比赛,随机性影响相对较小) +- **蒙特卡洛模拟参数**: + - **模拟次数**: 建议 10,000 - 100,000 次 (平衡精度和计算时间) + - **随机种子**: 固定种子用于可重复性,或使用时间种子用于随机性 + - **置信区间**: 通常使用 90% 或 95% 置信区间 + +#### 4. 时间调整系数 (f(T)) +- 将潜力转化为具体分数。原理:追分机会与剩余时间并非线性关系。 +- **常用函数形式**: `f(T) = (T / 48)^k`,其中 `k` 为衰减指数,`T` 为剩余分钟数。 +- **k 值的确定方法**: + 1. **历史数据拟合**: + - 收集过去 5 个赛季的所有比赛数据 + - 对每个 `(分差, 剩余时间)` 组合,统计实际追回分差 + - 使用非线性回归拟合 k 值: `实际追回分差 = a × (T/48)^k + b` + - 分阶段拟合: 第一节 (k₁)、第二节 (k₂)、第三节 (k₃)、第四节 (k₄)、最后 5 分钟 (k₅) + 2. **分阶段 k 值参考** (需根据实际数据调整): + - 第一节: k ≈ 1.0 (接近线性) + - 第二节: k ≈ 0.95 + - 第三节: k ≈ 0.90 + - 第四节: k ≈ 0.85 + - 最后 5 分钟: k ≈ 0.80 (时间越少,追分难度非线性增加) + 3. **分差调整**: + - 大分差 (≥15分): k 值可适当降低 (0.75~0.85),因为大分差追分更困难 + - 小分差 (<5分): k 值可适当提高 (0.85~0.95),因为小分差更容易逆转 + 4. **动态调整**: + - 根据实时比赛数据,每赛季重新拟合 k 值 + - 考虑不同球队风格对 k 值的影响 (快节奏球队 k 值可能略高) + +### 第二步:权重分配与动态调整 +权重 (W1, W2) 不是固定的,应随比赛进程动态变化。 +- **比赛早期 (第一节)**: W1 (基础潜力) 权重高 (如 0.7),W2 (实时状态) 权重低 (0.3)。样本小,球队基本盘更重要。 +- **比赛后期 (第四节)**: W2 权重急剧升高 (可达 0.6 以上),W1 降低。当前手感和势头决定一切。 +- **关键时刻 (最后 5 分钟)**: 可以引入球队关键时刻攻防效率数据来修正 F-base 或直接作为高权重特征。 + +**权重动态调整公式** (推荐): +- **基于剩余时间**: + - `W2 = min(0.3 + (48 - T) / 48 × 0.4, 0.7)`,其中 T 为剩余分钟数 + - `W1 = 1 - W2` +- **基于节次**: + - 第一节: W1 = 0.7, W2 = 0.3 + - 第二节: W1 = 0.6, W2 = 0.4 + - 第三节: W1 = 0.5, W2 = 0.5 + - 第四节: W1 = 0.4, W2 = 0.6 + - 最后 5 分钟: W1 = 0.3, W2 = 0.7 +- **基于分差** (可选调整): + - 大分差 (≥15分): 适当提高 W1 (基础实力更重要) + - 小分差 (<5分): 适当提高 W2 (实时状态更重要) + +### 第三步:实施路径与模拟 + +#### 1. 建立离线模型 (基础版) +- 用 Excel 或 Python,手动输入关键变量 (S, M, P 等)。 +- 运行公式,得到单次预测的预期追回分差 E。 +- **决策**: 如果 E ≥ 当前分差 X,则模型认为“有机会翻盘”。 + +#### 2. 引入蒙特卡洛模拟 (进阶版) +- 由于存在随机项 ε,单次计算意义不大。需要进行上万次模拟。 +- 在每次模拟中,为 ε 生成随机数,并运行公式,得到一个 E 值。 +- **模拟流程**: + 1. 设置模拟参数: + - 模拟次数: N = 10,000 (建议最小值) 或 100,000 (高精度) + - 随机波动项方差: σ (根据比赛阶段和分差确定) + - 随机种子: 可选,用于可重复性 + 2. 循环 N 次: + - 生成随机数: `ε ~ N(0, σ²)` + - 计算: `E = (F-base × W1 + F-live × W2) × f(T) + ε` + - 记录 E 值 + - **注意**: 随机波动项应在时间调整之后添加,反映最终结果的随机性 + 3. 统计分析: + - 计算 `E ≥ X` 的次数,翻盘概率 = `(E ≥ X 的次数) / N` + - 计算 E 的平均值: `E_mean = ΣE / N` + - 计算 E 的标准差: `E_std = sqrt(Σ(E - E_mean)² / N)` + - 计算分位数: 5%, 25%, 50%, 75%, 95% +- **统计结果**: + - 所有模拟结果中 `E ≥ X` 的比例,即为**动态翻盘概率**。 + - 所有 E 的平均值,即为**预期追回分差的期望值**。 + - E 的标准差,反映预测的不确定性。 + - 90% 置信区间: `[E_5%, E_95%]`,表示有 90% 的概率 E 落在此区间。 + - 分差变化区间预测: `[当前分差 - E_95%, 当前分差 - E_5%]` + +#### 3. 全自动实时模拟 (终极版) +- 通过 API 实时获取比赛数据 (Play-by-Play)。 +- 自动化计算 M, P 等实时特征。 +- 每 30 秒或每个回合结束后,自动触发一次蒙特卡洛模拟,输出更新的概率和预期分差。 + +**系统架构建议**: +- **数据层**: + - 实时数据获取服务 (API 轮询或 WebSocket) + - 数据缓存层 (Redis) 存储最近 N 分钟的数据 + - 数据存储层 (PostgreSQL/MongoDB) 存储历史数据 +- **计算层**: + - 特征计算服务 (实时计算 M, P 等特征) + - 模型预测服务 (加载模型,执行预测) + - 蒙特卡洛模拟服务 (并行计算,可分布式部署) +- **服务层**: + - API 网关 (统一接口) + - 预测结果缓存 (避免重复计算) + - 结果存储服务 (保存预测历史) +- **监控层**: + - 系统健康监控 (API 可用性、延迟) + - 模型性能监控 (预测准确率、置信度) + - 告警系统 (异常情况通知) + +**性能优化建议**: +- **并行计算**: 蒙特卡洛模拟使用多线程或分布式计算 +- **结果缓存**: 相同输入条件的预测结果缓存 10-30 秒 +- **模型优化**: 使用模型压缩技术 (量化、剪枝) 减少推理时间 +- **异步处理**: 非关键路径使用异步处理,提高响应速度 + +--- + +## 五、 应用示例 + +### 示例 1:比赛最后 3 分钟落后 10 分 + +**假设条件**: +| 模块 | 参数 | 假设值 | 说明 | +| :--- | :--- | :--- | :--- | +| **F-base** | 球队实力系数 (S) | 0.4 | 追分方实力稍强且阵容略占优 | +| **F-live** | 势头动量 (M) | -0.1 | 过去几分钟势头稍差 | +| **权重** | W1 (基础) | 0.4 | 比赛末期,实时状态权重大 | +| | W2 (实时) | 0.6 | | +| **时间** | 时间调整系数 f(3) | ≈ 0.13 | f(T) = (3/48)^0.9 | +| **随机项** | ε | 0 (暂定) | 首次计算忽略 | + +**单次模拟计算(忽略 ε)**: +`E = (0.4 * 0.4 + (-0.1) * 0.6) * 0.13 = (0.16 - 0.06) * 0.13 = 0.013` + +**结论**: 模型基于当前条件仅预期追回 0.013 分,远低于 10 分的分差,翻盘希望渺茫。 + +**蒙特卡洛模拟补充说明**: +- 假设随机波动项方差 σ = 1.0 (比赛后期) +- 进行 10,000 次模拟,每次生成 `ε ~ N(0, 1.0²)` +- 模拟结果统计: + - 预期追回分差期望值: `E_mean ≈ 0.013 + 0 = 0.013` (随机项均值为 0) + - 但考虑到随机性,实际 E 值分布: `E ~ N(0.013, 1.0²)` + - 翻盘概率: `P(E ≥ 10) = P(Z ≥ (10-0.013)/1.0) ≈ P(Z ≥ 9.99) ≈ 0%` + - 如果方差更大 (如 σ = 2.0),则 `P(E ≥ 10) ≈ 0.0001%` (几乎不可能) +- **修正**: 原结论中提到的 0.5% 翻盘概率和 2.5 分期望值需要更大的随机波动 (σ > 3.0) 才能实现,这在现实中不太合理 +- **更合理的结论**: 在 σ = 1.0 的情况下,翻盘概率 < 0.01%,表明需要极端随机事件(如连续多个三分、对手多次失误)才可能翻盘 + +### 示例 2:预测第三节最后 5 分钟(落后15分)到节末的追分情况 + +**输入参数表**: +| 模块 | 参数 | 假设值 | 说明 | +| :--- | :--- | :--- | :--- | +| **F-base** | 球队实力系数 (S) | 0.15 | 追分方净效率比对手每百回合多赢1.5分 | +| | 阵容对位系数 (L) | 0.0 | 双方阵容均衡 | +| | **F-base 小计** | **0.15** | F-base = S + L | +| **F-live** | 势头动量 (M) | -0.2 | 落后15分,近期势头在对手一方 | +| | 球星爆发因子 (P) | 0.0 | 无球星极度反常火热 | +| | **F-live 小计** | **-0.2** | F-live = M + P | +| **权重与时间**| W1 (基础) | 0.5 | 中后期,基本盘与实时状态并重 | +| | W2 (实时) | 0.5 | | +| | 剩余时间 (T) | 5 分钟 | | +| | 时间系数 f(5) | ≈ 0.175 | f(T) = (5/48)^0.9 | +| | 随机项 ε | 0 (暂定) | 首次计算忽略 | + +**计算过程**: +1. 综合潜力值 = `(0.15 * 0.5) + (-0.2 * 0.5) + 0 = 0.075 - 0.1 = -0.025` +2. 预期追回分差 (E) = `-0.025 * 0.175 ≈ -0.004` 分 + +**加入随机波动项 ε 并进行蒙特卡洛模拟后,可能的输出**: +| 输出指标 | 模拟可能结果示例 | 含义解读 | +| :--- | :--- | :--- | +| 预期追回分差平均值 | 约 -0.5 分到 +1.5 分 | 系统性追回大量分数的可能性很低 | +| 追回至少15分(扳平)的概率 | 低于 0.1% | 本节结束时翻盘的概率微乎其微 | +| 分差变化区间 (90%置信区间) | 从扩大5分到缩小7分 | 最可能结果仍是较大分差进入第四节 | + +> 批注[§★威★§6]: 该内容需要对比赛内容进行阶段切割,分析阶段性比分差情况。 + +--- + +## 六、 数据源与数据获取 + +### 数据需求清单 + +#### 1. 历史比赛数据 +- **数据内容**: + - 比赛基本信息: 日期、主客场、比分、分差 + - 每节比分和分差变化 + - Play-by-Play 数据 (每个回合的详细记录) + - 球队统计数据: 得分、篮板、助攻、失误等 +- **数据来源**: + - NBA Stats API (官方数据,免费但有限制) + - Basketball Reference (历史数据完整) + - ESPN API (实时数据) + - 第三方数据服务: Sportradar, Stats Perform + +#### 2. 球队统计数据 +- **数据内容**: + - 进攻效率、防守效率、净效率值 + - 比赛节奏 (Pace) + - 各项技术统计排名 + - 关键时刻 (Clutch) 数据 +- **更新频率**: 每场比赛后更新 + +#### 3. 球员统计数据 +- **数据内容**: + - 基础数据: 得分、篮板、助攻等 + - 高级数据: PER, TS%, BPM, VORP, DRPM + - 对位数据: 球员之间的对位效果 + - 阵容数据: 不同阵容组合的 Net Rating +- **更新频率**: 每场比赛后更新 + +#### 4. 实时比赛数据 +- **数据内容**: + - 实时比分和分差 + - Play-by-Play 实时更新 + - 球员在场情况 + - 暂停和挑战信息 +- **数据来源**: + - NBA Stats API (实时端点) + - ESPN API + - 第三方实时数据服务 +- **更新频率**: 每 30 秒或每个回合 + +### 免费 API 数据覆盖分析 + +#### NBA Stats API (官方免费 API) +- **基础 URL**: `https://stats.nba.com/stats/` +- **免费访问**: 是,但需要设置正确的请求头 +- **请求限制**: 无官方限制,但建议控制频率 (建议 < 10 请求/秒) + +##### 数据覆盖情况 + +**✅ 可直接获取的数据**: +1. **比赛基本信息**: + - `/scoreboard`: 赛程、比分、主客场 + - `/boxscore`: 比赛统计、每节比分 + - `/playbyplay`: Play-by-Play 数据 (每个回合) + - ✅ 满足需求: 历史比赛数据、实时比赛状态、分差变化 + +2. **球队统计数据**: + - `/teamdashboardbygeneralsplits`: 球队基础统计 + - `/teamdashboardbyclutch`: 关键时刻数据 + - `/teamdashboardbyteamperformance`: 球队表现数据 + - ✅ 满足需求: 进攻效率、防守效率、净效率值、比赛节奏 (Pace)、关键时刻数据 + +3. **球员统计数据**: + - `/playerdashboardbygeneralsplits`: 球员基础统计 + - `/playergamelog`: 球员单场数据 + - ✅ 满足需求: 得分、篮板、助攻、真实命中率 (TS%)、使用率 (USG%) + +4. **实时数据**: + - `/boxscore`: 实时比分 (需要轮询) + - `/playbyplay`: 实时 Play-by-Play (需要轮询) + - ✅ 满足需求: 实时分差、剩余时间、当前节次、球员在场情况 + +**⚠️ 需要计算的数据** (API 不直接提供,但可通过基础数据计算): +1. **PER (Player Efficiency Rating)**: + - ❌ API 不提供 + - ✅ 替代方案: 使用公式计算 `PER = (得分 + 篮板 + 助攻 + 抢断 + 盖帽 - 失误 - 投篮不中 - 罚球不中) / 上场时间 × 系数` + - 参考: [PER 计算公式](https://www.basketball-reference.com/about/per.html) + +2. **BPM (Box Plus/Minus)**: + - ❌ API 不提供 + - ✅ 替代方案: 使用简化版本,基于基础统计计算,或使用 Basketball Reference 爬取 + +3. **VORP (Value Over Replacement Player)**: + - ❌ API 不提供 + - ✅ 替代方案: 基于 BPM 和上场时间计算,或使用 Basketball Reference 爬取 + +4. **DRPM (Defensive Real Plus-Minus)**: + - ❌ API 不提供 + - ✅ 替代方案: 使用防守效率排名或防守统计作为代理指标 + +5. **阵容 Net Rating**: + - ❌ API 不直接提供阵容组合数据 + - ✅ 替代方案: + - 使用球队平均 Net Rating × 阵容调整系数 + - 阵容调整系数 = `Σ(球员在场时球队净效率值 - 球队平均净效率值) / 阵容球员数` + - 或使用 `/lineups` 端点 (如果可用) 获取阵容数据 + +6. **对位数据**: + - ❌ API 不提供球员对位数据 + - ✅ 替代方案: + - 使用防守球员的防守统计 (抢断、盖帽、防守篮板) 作为代理 + - 或基于历史比赛数据,统计特定对位的效果 + +**❌ 无法获取的数据** (需要替代方案): +1. **伤病报告和轮休信息**: + - ❌ API 不提供 + - ✅ 替代方案: + - 从 NBA 官网或 ESPN 爬取伤病报告 + - 或使用 Twitter API 监控球队官方账号 + - 或手动维护伤病数据库 + +2. **旅行距离和时区变化**: + - ❌ API 不提供 + - ✅ 替代方案: + - 使用 Google Maps API 计算城市间距离 (免费额度充足) + - 或维护城市坐标和时区数据库 + +3. **历史恩怨和特殊情节**: + - ❌ API 不提供 + - ✅ 替代方案: 基于历史对战记录和新闻数据手动标注 + +#### Basketball Reference (免费爬取) +- **数据格式**: HTML 页面 +- **优势**: + - 历史数据完整 (可追溯到 1946 年) + - 提供高级数据 (PER, BPM, VORP, DRPM) + - 完全免费 +- **劣势**: + - 需要爬虫解析 + - 有反爬虫机制 (需要控制频率) + - 实时数据更新较慢 +- **建议**: + - 用于获取历史数据和高级统计数据 + - 建议使用 `requests` + `BeautifulSoup` 或 `Scrapy` + - 控制请求频率 (建议 < 1 请求/秒) + - 缓存已获取的数据,避免重复请求 + +#### 数据获取策略建议 + +**阶段一: 历史数据收集** (一次性) +1. 使用 Basketball Reference 爬取过去 3-5 个赛季的历史数据 +2. 获取高级统计数据 (PER, BPM, VORP 等) +3. 存储到本地数据库 + +**阶段二: 实时数据更新** (持续) +1. 使用 NBA Stats API 获取实时比赛数据 +2. 每 30 秒轮询一次 (比赛进行时) +3. 计算实时特征 (M, P 等) + +**阶段三: 数据补充** (按需) +1. 伤病信息: 手动维护或爬取 NBA 官网 +2. 旅行距离: 使用 Google Maps API 或维护静态数据库 +3. 高级统计: 定期从 Basketball Reference 更新 + +### 数据需求对照表 + +| 数据需求 | 来源 | 获取方式 | 难度 | 备注 | +|---------|------|---------|------|------| +| **赛前预测数据** | +| 比赛基本信息 | NBA Stats API | 直接获取 | ⭐ 简单 | `/scoreboard`, `/boxscore` | +| 球队进攻/防守效率 | NBA Stats API | 直接获取 | ⭐ 简单 | `/teamdashboardbygeneralsplits` | +| 球队净效率值 | NBA Stats API | 直接获取 | ⭐ 简单 | 计算: 进攻效率 - 防守效率 | +| 比赛节奏 (Pace) | NBA Stats API | 直接获取 | ⭐ 简单 | `/teamdashboardbygeneralsplits` | +| 球员基础统计 | NBA Stats API | 直接获取 | ⭐ 简单 | `/playerdashboardbygeneralsplits` | +| 球员真实命中率 (TS%) | NBA Stats API | 计算 | ⭐⭐ 中等 | 基于得分、投篮次数、罚球次数计算 | +| 球员使用率 (USG%) | NBA Stats API | 计算 | ⭐⭐ 中等 | 基于投篮、失误、罚球计算 | +| PER | Basketball Reference | 爬取或计算 | ⭐⭐⭐ 较难 | 公式复杂,建议爬取 | +| BPM | Basketball Reference | 爬取 | ⭐⭐⭐ 较难 | 需要爬取,或使用简化版本 | +| VORP | Basketball Reference | 爬取 | ⭐⭐⭐ 较难 | 基于 BPM 计算,建议爬取 | +| DRPM | Basketball Reference | 爬取 | ⭐⭐⭐ 较难 | 需要爬取,或用防守统计代理 | +| 阵容 Net Rating | NBA Stats API | 计算 | ⭐⭐ 中等 | 基于球员在场数据计算 | +| 对位数据 | - | 计算或爬取 | ⭐⭐⭐ 较难 | 基于历史数据统计,或爬取 | +| 伤病报告 | NBA 官网 | 爬取 | ⭐⭐⭐ 较难 | 需要爬取或手动维护 | +| 旅行距离 | Google Maps API | 计算 | ⭐ 简单 | 免费额度充足 | +| 背靠背信息 | NBA Stats API | 计算 | ⭐ 简单 | 基于赛程计算 | +| **赛中实时数据** | +| 实时比分和分差 | NBA Stats API | 直接获取 | ⭐ 简单 | `/boxscore` (轮询) | +| 剩余时间 | NBA Stats API | 直接获取 | ⭐ 简单 | `/boxscore` | +| 当前节次 | NBA Stats API | 直接获取 | ⭐ 简单 | `/boxscore` | +| Play-by-Play | NBA Stats API | 直接获取 | ⭐ 简单 | `/playbyplay` (轮询) | +| 球员在场情况 | NBA Stats API | 直接获取 | ⭐ 简单 | `/playbyplay` | +| 暂停次数 | NBA Stats API | 直接获取 | ⭐ 简单 | `/boxscore` | +| 势头动量 (M) | NBA Stats API | 计算 | ⭐⭐ 中等 | 基于 Play-by-Play 计算 | +| 球星爆发因子 (P) | NBA Stats API | 计算 | ⭐⭐ 中等 | 基于本场 TS% 和赛季平均计算 | + +**难度说明**: +- ⭐ 简单: 直接 API 调用或简单计算 +- ⭐⭐ 中等: 需要数据处理和计算 +- ⭐⭐⭐ 较难: 需要爬取或复杂计算 + +### 免费 API 使用注意事项 + +1. **请求频率控制**: + - NBA Stats API: 建议 < 10 请求/秒 + - Basketball Reference: 建议 < 1 请求/秒 + - 使用请求队列和延迟机制 + +2. **请求头设置** (NBA Stats API): + ```python + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Referer': 'https://www.nba.com/', + 'Accept': 'application/json' + } + ``` + +3. **数据缓存**: + - 历史数据: 永久缓存 + - 实时数据: 缓存 30-60 秒 + - 统计数据: 缓存到下次比赛后更新 + +4. **错误处理**: + - 实现重试机制 (指数退避) + - 处理 API 临时不可用的情况 + - 记录失败请求,后续补全数据 + +### 数据存储建议 + +#### 数据库选择 +- **PostgreSQL**: 适合结构化数据,支持复杂查询 +- **MongoDB**: 适合 Play-by-Play 等非结构化数据 +- **Redis**: 用于缓存实时数据和计算结果 +- **时序数据库** (InfluxDB/TimescaleDB): 适合存储时间序列数据 (如实时分差变化) + +#### 数据表设计 (示例) +- `games`: 比赛基本信息 +- `game_stats`: 比赛统计数据 +- `play_by_play`: 回合数据 +- `team_stats`: 球队统计数据 +- `player_stats`: 球员统计数据 +- `lineup_stats`: 阵容统计数据 +- `predictions`: 预测结果存储 + +#### 数据质量保证 +- **数据验证规则**: + - 比分合理性检查 (单节得分不超过 50 分,单场得分不超过 200 分) + - 时间戳连续性检查 + - 球员在场时间合理性 (不超过比赛总时间) + - 统计数据一致性 (得分 = 两分 + 三分 + 罚球) +- **缺失值处理策略**: + - 对位数据缺失: 使用防守球员 DRPM 和进攻球员 ORPM 作为代理 + - 阵容 Net Rating 缺失: 使用球队平均 Net Rating × 阵容调整系数 + - 实时数据延迟: 使用最近一次有效数据,并标记数据时效性 + - 历史数据缺失: 使用赛季平均值或联盟平均值填充 + +--- + +## 七、 实施路线图 + +### 阶段一: 数据收集与基线模型 (1-2 个月) + +#### 1.1 数据收集 +- [ ] 接入 NBA Stats API 或第三方数据源 +- [ ] 收集过去 3-5 个赛季的历史比赛数据 +- [ ] 建立数据存储和更新机制 +- [ ] 数据清洗和预处理 + +#### 1.2 基线模型实现 +- [ ] 实现历史概率速查表 +- [ ] 按 `(分差, 剩余时间)` 组合统计翻盘概率 +- [ ] 建立查询接口,支持实时查询 + +#### 1.3 验证与优化 +- [ ] 使用历史数据验证模型准确性 +- [ ] 计算模型评估指标 (准确率、AUC 等) +- [ ] 优化数据分组粒度 (分差区间、时间区间) + +### 阶段二: 进阶模型开发 (2-3 个月) + +#### 2.1 球队能力调整 +- [ ] 实现球队实力系数计算 +- [ ] 实现阵容对位系数计算 +- [ ] 实现归一化函数 +- [ ] 在基线模型基础上加入能力调整 + +#### 2.2 实时状态计算 +- [ ] 实现势头动量 (M) 计算 +- [ ] 实现球星爆发因子 (P) 计算 +- [ ] 实现实时特征提取 + +#### 2.3 时间调整系数拟合 +- [ ] 使用历史数据拟合 k 值 +- [ ] 分阶段 (按节次) 拟合 k 值 +- [ ] 验证 k 值的合理性 + +### 阶段三: 蒙特卡洛模拟 (1-2 个月) + +#### 3.1 模拟框架实现 +- [ ] 实现随机波动项生成 +- [ ] 实现蒙特卡洛模拟核心逻辑 +- [ ] 实现统计结果计算 (概率、期望值、置信区间) + +#### 3.2 性能优化 +- [ ] 优化模拟速度 (并行计算、向量化) +- [ ] 实现结果缓存机制 +- [ ] 优化内存使用 + +#### 3.3 验证与调优 +- [ ] 使用历史数据验证模拟结果 +- [ ] 调整随机波动项方差 +- [ ] 优化模拟次数 (平衡精度和速度) + +### 阶段四: 机器学习模型 (3-4 个月) + +#### 4.1 特征工程 +- [ ] 实现所有特征的计算逻辑 +- [ ] 特征选择和重要性分析 +- [ ] 特征标准化和归一化 + +#### 4.2 模型训练 +- [ ] 数据准备和划分 (训练集、验证集、测试集) +- [ ] 模型选择和超参数调优 +- [ ] 模型训练和评估 + +#### 4.3 模型部署 +- [ ] 模型序列化和加载 +- [ ] 实时预测接口开发 +- [ ] 模型更新和重训练机制 +- [ ] 模型版本管理 (使用 MLflow 或类似工具) +- [ ] A/B 测试框架搭建 +- [ ] 模型解释性工具集成 (SHAP/LIME) + +### 阶段五: 全自动实时系统 (2-3 个月) + +#### 5.1 实时数据接入 +- [ ] 实现实时数据获取 (API 轮询或 WebSocket) +- [ ] 实现数据解析和存储 +- [ ] 实现数据质量检查 + +#### 5.2 自动化预测 +- [ ] 实现自动触发预测 (每 30 秒或每回合) +- [ ] 实现预测结果存储和展示 +- [ ] 实现异常处理和告警 + +#### 5.3 系统优化 +- [ ] 性能优化 (延迟、吞吐量) +- [ ] 系统监控和日志 +- [ ] 用户体验优化 +- [ ] 实现降级策略 (API 失败时的备用方案) +- [ ] 实现限流和熔断机制 +- [ ] 建立数据备份和恢复机制 + +### 总体时间估算 +- **MVP 版本** (阶段一 + 阶段二基础): 3-4 个月 +- **完整版本** (所有阶段): 12-18 个月 (考虑数据获取、模型调试和优化时间) + + +--- + +## 八、 模型评估指标 + +### 分类任务评估 (翻盘预测) + +#### 1. 准确率 (Accuracy) +- **定义**: `(正确预测数) / (总预测数)` +- **目标**: > 60% (基准线为 50%) + +#### 2. 精确率 (Precision) 和召回率 (Recall) +- **精确率**: `(预测翻盘且实际翻盘) / (预测翻盘总数)` +- **召回率**: `(预测翻盘且实际翻盘) / (实际翻盘总数)` +- **目标**: 精确率 > 55%, 召回率 > 50% + +#### 3. F1 分数 +- **定义**: `F1 = 2 × (精确率 × 召回率) / (精确率 + 召回率)` +- **目标**: > 0.52 + +#### 4. AUC-ROC +- **定义**: ROC 曲线下面积,衡量模型区分能力 +- **目标**: > 0.65 (基准线为 0.5) + +#### 5. 校准度 (Calibration) +- **定义**: 预测概率与实际概率的一致性 +- **方法**: Brier Score, Calibration Plot +- **目标**: Brier Score < 0.25 + +### 回归任务评估 (分差预测) + +#### 1. 平均绝对误差 (MAE) +- **定义**: `MAE = Σ|预测值 - 实际值| / N` +- **目标**: < 3.0 分 + +#### 2. 均方根误差 (RMSE) +- **定义**: `RMSE = sqrt(Σ(预测值 - 实际值)² / N)` +- **目标**: < 4.0 分 + +#### 3. 平均绝对百分比误差 (MAPE) +- **定义**: `MAPE = Σ|(预测值 - 实际值) / 实际值| / N × 100%` +- **目标**: < 30% + +### 概率预测评估 + +#### 1. Brier Score +- **定义**: `BS = Σ(预测概率 - 实际结果)² / N` +- **范围**: [0, 1],越小越好 +- **目标**: < 0.25 + +#### 2. Log Loss +- **定义**: `Log Loss = -Σ[y × log(p) + (1-y) × log(1-p)] / N` +- **范围**: [0, ∞),越小越好 +- **目标**: < 0.7 + +### 模型对比基准 + +#### 1. 基线模型 +- 历史概率速查表 +- 准确率目标: > 55% + +#### 2. 进阶模型 +- 基线模型 + 球队能力调整 +- 准确率目标: > 58% + +#### 3. 机器学习模型 +- 完整特征工程 + XGBoost/LightGBM +- 准确率目标: > 60% + +--- + +## 九、 注意事项与局限性 + +### 数据局限性 + +1. **数据质量**: + - 历史数据可能存在缺失或错误 + - 实时数据可能有延迟 + - 需要建立数据质量检查机制 + +2. **数据覆盖**: + - 某些特征 (如对位数据、阵容数据) 可能不完整 + - 需要处理缺失值 + +3. **数据时效性**: + - 球队和球员状态会随时间变化 + - 需要定期更新模型和数据 + +### 模型局限性 + +1. **过拟合风险**: + - 特征过多可能导致过拟合 + - 需要使用交叉验证和正则化 + +2. **样本不平衡**: + - 翻盘情况相对较少,可能导致模型偏向预测"不翻盘" + - 需要使用采样技术 (SMOTE, 过采样等) + +3. **外推能力**: + - 模型基于历史数据训练,可能无法预测极端情况 + - 需要设置合理的预测范围 + +### 实际应用注意事项 + +1. **实时性要求**: + - 预测需要在短时间内完成 (建议 < 1 秒) + - 需要优化计算性能 + +2. **系统稳定性**: + - API 可能不稳定,需要实现重试和降级机制 + - 需要监控系统健康状态 + +3. **用户体验**: + - 预测结果需要清晰易懂 + - 需要提供置信区间和不确定性说明 + +4. **合规性**: + - 如果用于投注或交易,需要遵守相关法律法规 + - 需要明确说明模型的局限性和风险 + +--- + +## 十、 关键问题与改进建议 + +### 发现的问题 + +#### 1. 公式逻辑问题 +- **问题**: 原公式中随机波动项的位置可能不合理 +- **修正**: 随机波动项应在时间调整之后添加,即 `E = (F-base × W1 + F-live × W2) × f(T) + ε` +- **理由**: 随机性影响的是最终分差结果,而不是追分潜力本身 + +#### 2. 归一化方法选择 +- **问题**: 提供了多种归一化方法,但未说明选择标准 +- **建议**: + - 统一使用 Z-score 归一化(推荐),便于解释和比较 + - 或使用 Min-Max 归一化到 [-1, 1],便于控制范围 + - 避免在同一模型中混用不同归一化方法 + +#### 3. 权重确定方法 +- **问题**: 权重 W1 和 W2 的调整规则不够明确 +- **改进**: 提供了基于剩余时间和节次的动态调整公式 + +#### 4. 数据获取可行性 +- **问题**: 某些数据(如对位数据、阵容 Net Rating、高级统计)可能难以获取 +- **解决方案** (基于免费 API): + - **对位数据**: + - 如果历史对位数据不足,使用防守球员的防守统计(抢断、盖帽、防守篮板)作为代理 + - 或基于历史比赛数据,统计特定对位的效果(需要积累数据) + - **阵容 Net Rating**: + - 使用球队平均 Net Rating × 阵容调整系数 + - 阵容调整系数 = `Σ(球员在场时球队净效率值 - 球队平均净效率值) / 阵容球员数` + - 或从 Basketball Reference 爬取阵容数据 + - **高级统计 (PER, BPM, VORP, DRPM)**: + - 优先从 Basketball Reference 爬取(一次性收集历史数据) + - 或使用简化公式计算(精度略低但可用) + - **伤病信息**: + - 从 NBA 官网或 ESPN 爬取 + - 或使用 Twitter API 监控球队官方账号(免费额度有限) + - 或手动维护伤病数据库 + +#### 5. 模型评估目标值 +- **问题**: 评估指标的目标值缺乏依据 +- **建议**: + - 目标值应该基于基线模型(随机猜测或简单规则)的表现来确定 + - 建议先运行基线模型,获得基准性能,再设定合理的提升目标(如提升 5-10%) + +#### 6. 特征数量过多 +- **问题**: 150-300 个特征可能导致过拟合 +- **建议**: + - 使用特征选择技术(如 LASSO、特征重要性排序) + - 考虑使用降维技术(PCA、ICA) + - 建议最终使用 50-100 个最重要的特征 + +#### 7. 时间估算可能过于乐观 +- **问题**: 9-14 个月可能低估了数据获取和模型调试的时间 +- **建议**: + - 数据获取阶段可能需要额外 1-2 个月(API 接入、数据清洗) + - 模型调试和优化可能需要额外 2-3 个月 + - 建议总时间估算为 12-18 个月 + +### 补充建议 + +#### 1. 添加数据验证机制 +- 数据质量检查: 异常值检测、缺失值处理、数据一致性验证 +- 实时数据验证: API 响应时间监控、数据延迟检测、数据完整性检查 + +#### 2. 模型解释性 +- 使用 SHAP 值或 LIME 解释模型预测 +- 提供特征重要性排序 +- 可视化关键特征对预测结果的影响 + +#### 3. A/B 测试框架 +- 设计 A/B 测试来验证模型效果 +- 对比不同模型版本的表现 +- 收集用户反馈并迭代改进 + +#### 4. 风险控制 +- 设置预测置信度阈值,低于阈值时不给出预测 +- 监控模型性能,如果准确率下降超过阈值,触发告警 +- 实现模型回滚机制 + + +#### 6. 法律和伦理考虑 +- 如果用于投注,需要遵守相关法律法规 +- 数据使用需要遵守隐私保护规定 +- 模型预测结果的使用需要明确免责声明 \ No newline at end of file diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/NbaStatsApi.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/NbaStatsApi.kt new file mode 100644 index 0000000..1b67a2c --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/NbaStatsApi.kt @@ -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 +} + +/** + * NBA Stats API Scoreboard 响应 + */ +data class ScoreboardResponse( + val resultSets: List +) + +/** + * Result Set + */ +data class ResultSet( + val name: String, + val headers: List, + val rowSet: List> +) + +/** + * 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? +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketGammaApi.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketGammaApi.kt index fa87527..2bc27d1 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketGammaApi.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/api/PolymarketGammaApi.kt @@ -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> + + /** + * 根据条件获取市场信息 * 文档: 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? = null, - @Query("include_tag") includeTag: Boolean? = null + @Query("include_tag") includeTag: Boolean? = null, + @Query("tags") tags: List? = 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? = null, ): Response> } +/** + * 体育元数据响应 + * 文档: 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? = 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 ) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaGameController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaGameController.kt new file mode 100644 index 0000000..e3d6ebe --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaGameController.kt @@ -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> { + 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>> { + 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)) + } + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaMarketController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaMarketController.kt new file mode 100644 index 0000000..c99ffb7 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaMarketController.kt @@ -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> { + 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>> { + 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)) + } + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaQuantitativeStrategyController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaQuantitativeStrategyController.kt new file mode 100644 index 0000000..776eb98 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/nba/NbaQuantitativeStrategyController.kt @@ -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> { + 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> { + 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> { + 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> { + 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> { + 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? +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaGameDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaGameDto.kt new file mode 100644 index 0000000..9cf03cf --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaGameDto.kt @@ -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, + val total: Long +) + +/** + * NBA 比赛列表请求 + * 前端传递时间戳(毫秒),后端转换为西8区时间 + */ +data class NbaGameListRequest( + val startTimestamp: Long? = null, // 开始时间戳(毫秒) + val endTimestamp: Long? = null, // 结束时间戳(毫秒) + val gameStatus: String? = null +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaMarketDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaMarketDto.kt new file mode 100644 index 0000000..0ef5cde --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaMarketDto.kt @@ -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, + val total: Long +) + +/** + * NBA 市场列表请求 + */ +data class NbaMarketListRequest( + val active: Boolean? = true, + val closed: Boolean? = false, + val archived: Boolean? = false +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaQuantitativeStrategyDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaQuantitativeStrategyDto.kt new file mode 100644 index 0000000..0316f60 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/NbaQuantitativeStrategyDto.kt @@ -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?, + 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? = 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? = 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, + val total: Long, + val page: Int, + val limit: Int +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaGame.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaGame.kt new file mode 100644 index 0000000..b2346db --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaGame.kt @@ -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() +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaMarket.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaMarket.kt new file mode 100644 index 0000000..da25ec3 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaMarket.kt @@ -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() +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaQuantitativeStrategy.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaQuantitativeStrategy.kt new file mode 100644 index 0000000..ffe706d --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaQuantitativeStrategy.kt @@ -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() +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaStrategyStatistics.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaStrategyStatistics.kt new file mode 100644 index 0000000..bee2bb6 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaStrategyStatistics.kt @@ -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() +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaTradingSignal.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaTradingSignal.kt new file mode 100644 index 0000000..710d493 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/NbaTradingSignal.kt @@ -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() +) + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/enums/SportsTagId.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/enums/SportsTagId.kt new file mode 100644 index 0000000..afc64e3 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/enums/SportsTagId.kt @@ -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 { + return values().map { it.tagId } + } + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaGameRepository.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaGameRepository.kt new file mode 100644 index 0000000..a1137e2 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaGameRepository.kt @@ -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 { + fun findByNbaGameId(nbaGameId: String): NbaGame? + fun findByGameDate(gameDate: LocalDate): List + fun findByGameDateBetween(startDate: LocalDate, endDate: LocalDate): List + fun findByGameStatus(gameStatus: String): List + fun findByHomeTeamAndAwayTeamAndGameDate(homeTeam: String, awayTeam: String, gameDate: LocalDate): NbaGame? + fun findByPolymarketMarketId(polymarketMarketId: String): NbaGame? + + /** + * 查询最新的比赛(按创建时间倒序) + */ + fun findFirstByOrderByCreatedAtDesc(): NbaGame? + + /** + * 根据创建时间查询比赛 + */ + fun findByCreatedAtGreaterThan(createdAt: Long): List +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaMarketRepository.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaMarketRepository.kt new file mode 100644 index 0000000..7bf065c --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaMarketRepository.kt @@ -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 { + fun findByConditionId(conditionId: String): NbaMarket? + fun findByPolymarketMarketId(polymarketMarketId: String): NbaMarket? + fun findByActiveAndClosed(active: Boolean, closed: Boolean): List + fun findByCategory(category: String): List + fun findByActive(active: Boolean): List +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaQuantitativeStrategyRepository.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaQuantitativeStrategyRepository.kt new file mode 100644 index 0000000..89b9972 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaQuantitativeStrategyRepository.kt @@ -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 { + fun findByAccountId(accountId: Long): List + fun findByAccountIdAndEnabled(accountId: Long, enabled: Boolean): List + fun findByEnabled(enabled: Boolean): List + fun findByStrategyName(strategyName: String): NbaQuantitativeStrategy? +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaStrategyStatisticsRepository.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaStrategyStatisticsRepository.kt new file mode 100644 index 0000000..5928952 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaStrategyStatisticsRepository.kt @@ -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 { + fun findByStrategyId(strategyId: Long): List + fun findByStrategyIdAndStatDate(strategyId: Long, statDate: LocalDate): NbaStrategyStatistics? + fun findByStrategyIdAndStatDateBetween(strategyId: Long, startDate: LocalDate, endDate: LocalDate): List +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaTradingSignalRepository.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaTradingSignalRepository.kt new file mode 100644 index 0000000..50660f4 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/NbaTradingSignalRepository.kt @@ -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 { + fun findByStrategyId(strategyId: Long): List + fun findByGameId(gameId: Long): List + fun findByMarketId(marketId: Long): List + fun findBySignalType(signalType: String): List + fun findBySignalStatus(signalStatus: String): List + fun findByStrategyIdAndSignalType(strategyId: Long, signalType: String): List + fun findByStrategyIdAndCreatedAtBetween(strategyId: Long, startTime: Long, endTime: Long): List +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameService.kt new file mode 100644 index 0000000..c4e241e --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameService.kt @@ -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 { + 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): 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 { + // 从 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() + 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) { + 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> { + 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() + 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 + } + } + } + +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketNameParser.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketNameParser.kt new file mode 100644 index 0000000..1a75088 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketNameParser.kt @@ -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 { + val teams = mutableListOf() + + // 常见的球队名称模式 + 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) + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketService.kt new file mode 100644 index 0000000..d8e65c8 --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaMarketService.kt @@ -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> { + // 直接使用枚举中定义的 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 { + 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> { + 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() + + 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) + } + } + +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaQuantitativeStrategyService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaQuantitativeStrategyService.kt new file mode 100644 index 0000000..dd1cb4f --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/nba/NbaQuantitativeStrategyService.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 + ) + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt index 3994a9e..36003f0 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt @@ -28,5 +28,29 @@ object JsonUtils { emptyList() } } + + /** + * 解析 JSON 字符串列表(parseStringArray 的别名) + */ + fun parseStringList(jsonString: String?): List { + 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 + } + } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/NbaApiValidator.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/NbaApiValidator.kt new file mode 100644 index 0000000..f69e40e --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/NbaApiValidator.kt @@ -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 + } + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt index 7a8542f..1a58237 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt @@ -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) + } } /** diff --git a/backend/src/main/resources/db/migration/V11__create_nba_quantitative_trading_tables.sql b/backend/src/main/resources/db/migration/V11__create_nba_quantitative_trading_tables.sql new file mode 100644 index 0000000..5d510de --- /dev/null +++ b/backend/src/main/resources/db/migration/V11__create_nba_quantitative_trading_tables.sql @@ -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策略执行统计表'; + diff --git a/backend/src/main/resources/db/migration/V12__remove_nba_markets_foreign_keys.sql b/backend/src/main/resources/db/migration/V12__remove_nba_markets_foreign_keys.sql new file mode 100644 index 0000000..f65122d --- /dev/null +++ b/backend/src/main/resources/db/migration/V12__remove_nba_markets_foreign_keys.sql @@ -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; + diff --git a/backend/src/test/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameServiceTest.kt b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameServiceTest.kt new file mode 100644 index 0000000..25023b1 --- /dev/null +++ b/backend/src/test/kotlin/com/wrbug/polymarketbot/service/nba/NbaGameServiceTest.kt @@ -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() + } + } + } +} + diff --git a/docs/zh/nba-api-integration-technical-solution.md b/docs/zh/nba-api-integration-technical-solution.md new file mode 100644 index 0000000..10f73b6 --- /dev/null +++ b/docs/zh/nba-api-integration-technical-solution.md @@ -0,0 +1,999 @@ +# NBA 量化交易系统技术方案 + +## 一、概述 + +### 1.1 产品定位 + +本系统是一个基于 NBA 比赛数据的量化交易系统,用户在前端配置量化策略参数,系统后台实时获取 NBA 比赛数据,根据配置的策略参数进行量化分析,自动生成买入/卖出信号,并通过 WebSocket 实时推送给前端展示。 + +### 1.2 产品流程 + +``` +用户在前端配置策略参数 + ↓ +后台保存配置并启动量化任务 + ↓ +后台实时获取 NBA 比赛数据 + ↓ +后台执行量化分析逻辑 + ↓ +生成买入/卖出信号 + ↓ +通过 WebSocket 推送给前端 + ↓ +前端展示交易信号和结果 +``` + +### 1.3 核心功能 + +1. **策略配置管理**:用户在前端配置量化策略参数(如触发条件、买入卖出规则、风险控制参数等) +2. **数据获取**:后台实时获取 NBA 比赛数据、球队统计、球员统计等 +3. **量化分析**:根据配置的策略参数和实时数据,执行量化分析逻辑 +4. **信号生成**:生成买入/卖出信号,包含市场、方向、价格、数量等信息 +5. **实时推送**:通过 WebSocket 将交易信号实时推送给前端 +6. **结果展示**:前端展示交易信号、执行结果、统计数据等 + +### 1.4 技术栈 + +- **后端框架**: Spring Boot 3.2.0 + Kotlin +- **HTTP 客户端**: Retrofit 2.9.0 + OkHttp 4.12.0 +- **数据存储**: MySQL 8.2.0(结构化数据)+ Redis(缓存) +- **爬虫框架**: Jsoup(用于 Basketball Reference) +- **任务调度**: Spring Scheduler(定时任务) +- **WebSocket**: Spring WebSocket(实时推送) + +--- + +## 二、系统架构设计 + +### 2.1 整体架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 前端层 (Frontend Layer) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 策略配置页面 │ │ 交易信号展示 │ │ 结果统计页面 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ HTTP API / WebSocket +┌─────────────────────────────────────────────────────────────┐ +│ 应用层 (Application Layer) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 策略配置API │ │ 量化任务API │ │ WebSocket │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────┐ +│ 服务层 (Service Layer) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 策略配置服务 │ │ 量化分析服务 │ │ 数据推送服务 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ NBA数据服务 │ │ 信号生成服务 │ │ 任务调度服务 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────┐ +│ 数据层 (Data Layer) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ NBA API │ │ 数据存储 │ │ 缓存服务 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 模块划分 + +#### 2.2.1 前端模块 + +- **策略配置模块**:用户配置量化策略参数 +- **交易信号展示模块**:实时展示买入/卖出信号 +- **结果统计模块**:展示交易结果和统计数据 + +#### 2.2.2 后端模块 + +- **策略配置管理模块**:管理用户配置的策略参数 +- **NBA 数据获取模块**:从 NBA Stats API 和 Basketball Reference 获取数据 +- **量化分析模块**:执行量化分析逻辑,生成交易信号 +- **信号推送模块**:通过 WebSocket 推送交易信号 +- **任务调度模块**:管理量化任务的启动、停止、调度 + +--- + +## 三、产品功能设计 + +### 3.1 策略配置功能 + +#### 3.1.1 配置参数 + +用户在前端可以配置以下参数: + +**基础配置**: +- 策略名称:用户自定义策略名称 +- 启用状态:是否启用该策略 +- 关联账户:选择用于交易的账户 + +**触发条件配置**: +- 比赛筛选:选择关注的比赛(可按球队、日期、重要性等筛选) +- 数据指标:选择用于分析的指标(如分差、剩余时间、球队实力等) +- 触发阈值:设置触发买入/卖出的阈值条件 + +**交易规则配置**: +- 买入规则:配置买入条件、买入金额、买入时机等 +- 卖出规则:配置卖出条件、卖出金额、卖出时机等 +- 价格策略:配置价格计算方式(如固定价格、动态价格等) + +**风险控制配置**: +- 最大持仓:单次最大买入金额 +- 最小持仓:单次最小买入金额 +- 每日亏损限制:每日最大亏损金额 +- 每日订单限制:每日最大订单数量 +- 价格容忍度:允许的价格偏差范围 + +**高级配置**: +- 数据更新频率:NBA 数据更新频率(如 30 秒、1 分钟等) +- 分析频率:量化分析执行频率 +- 推送设置:是否推送失败订单、推送频率等 + +#### 3.1.2 配置管理 + +- **创建策略**:用户在前端创建新的量化策略配置 +- **编辑策略**:用户可以修改已有策略的配置参数 +- **删除策略**:用户可以删除不需要的策略 +- **启用/禁用策略**:用户可以启用或禁用策略,禁用后停止执行量化分析 +- **策略列表**:展示所有策略配置,支持搜索、筛选、排序 + +### 3.2 量化分析功能 + +#### 3.2.1 数据获取 + +系统后台实时获取以下数据: + +**比赛数据**: +- 实时比分和分差 +- 比赛状态(未开始/进行中/已结束) +- 当前节次和剩余时间 +- Play-by-Play 数据(每个回合的详细记录) + +**统计数据**: +- 球队统计数据(进攻效率、防守效率、净效率值等) +- 球员统计数据(得分、篮板、助攻、真实命中率等) +- 高级统计数据(PER、BPM、VORP、DRPM 等) + +**历史数据**: +- 历史比赛数据 +- 历史对战记录 +- 历史统计数据 + +#### 3.2.2 量化分析逻辑 + +系统根据用户配置的策略参数和实时获取的 NBA 数据,执行量化分析: + +**核心思想**: +- 不是简单的条件判断(如"主队落后买入") +- 而是综合分析两支队伍的实力、状态、对位关系等因素 +- 计算每支队伍的综合评分和获胜概率 +- 评估交易价值和风险 +- 选择最优的交易方向和时机 + +**数据预处理**: +- 数据清洗和验证 +- 数据标准化和归一化 +- 特征工程(提取关键特征) + +**综合评分计算**: +- 计算主队和客队的基础实力评分(净效率值、攻防效率、节奏等) +- 计算近期状态评分(近期胜率、净效率值变化、势头等) +- 计算阵容完整度评分(基于缺失球员的VORP) +- 计算球星状态评分(PER、TS%、健康状况等) +- 计算环境因素评分(主客场、休息天数、背靠背等) +- 综合计算主队和客队的综合评分 + +**对位分析**: +- 分析球星对位优势(防守限制效果、历史对位数据) +- 分析阵容克制关系(内线、外线、快攻优势) +- 计算对位综合评分 + +**实时状态分析**(比赛进行中): +- 分析当前分差和剩余时间 +- 分析势头动量(过去N回合的净胜分) +- 实时调整综合评分 + +**获胜概率计算**: +- 基于综合评分计算基础获胜概率 +- 根据对位优势调整概率 +- 根据实时状态调整概率(比赛进行中) +- 最终得到主队和客队的获胜概率 + +**交易价值计算**: +- 计算预期收益(基于获胜概率和价格) +- 计算风险调整收益(考虑不确定性、价格波动、流动性) +- 计算交易价值评分 + +**策略执行**: +- 根据配置的触发条件(最小获胜概率差异、最小交易价值等),判断是否满足买入/卖出条件 +- 根据配置的交易规则,计算买入/卖出价格和数量 +- 根据配置的风险控制参数,验证交易是否合规 + +**信号生成**: +- 生成买入信号:包含市场 ID、方向(YES/NO,由系统自动判断)、价格、数量、原因等 +- 生成卖出信号:包含市场 ID、方向、价格、数量、原因等 +- 信号验证:验证信号的有效性和合规性 + +### 3.3 信号推送功能 + +#### 3.3.1 推送方式 + +系统通过 WebSocket 实时推送交易信号给前端: + +**推送内容**: +- 买入信号:市场信息、方向、价格、数量、触发原因、时间戳等 +- 卖出信号:市场信息、方向、价格、数量、触发原因、时间戳等 +- 信号状态:信号生成、信号执行中、信号执行成功、信号执行失败等 + +**推送频率**: +- 实时推送:信号生成后立即推送 +- 批量推送:多个信号可以批量推送(减少网络开销) + +#### 3.3.2 订阅管理 + +前端通过 WebSocket 订阅交易信号: + +**订阅方式**: +- 订阅所有策略的信号 +- 订阅特定策略的信号 +- 订阅特定市场的信号 + +**订阅管理**: +- 前端可以动态订阅/取消订阅 +- 支持多个前端客户端同时订阅 +- 连接断开后自动重连和恢复订阅 + +### 3.4 结果展示功能 + +#### 3.4.1 实时信号展示 + +前端实时展示交易信号: + +**信号列表**: +- 展示所有生成的交易信号 +- 支持按策略、市场、时间等筛选 +- 支持按时间、价格等排序 + +**信号详情**: +- 展示信号的详细信息(市场、方向、价格、数量、原因等) +- 展示信号的执行状态和结果 +- 展示信号的历史记录 + +#### 3.4.2 统计展示 + +前端展示交易统计: + +**策略统计**: +- 每个策略的信号数量 +- 每个策略的成功率 +- 每个策略的盈亏情况 + +**总体统计**: +- 总信号数量 +- 总成功率 +- 总盈亏情况 +- 每日/每周/每月统计 + +--- + +## 四、数据源集成方案 + +### 4.1 NBA Stats API 集成 + +#### 4.1.1 API 基础信息 + +- **Base URL**: `https://stats.nba.com/stats/` +- **认证**: 无需认证,但需要设置正确的请求头 +- **请求限制**: 建议 < 10 请求/秒 +- **数据格式**: JSON + +#### 4.1.2 请求头配置 + +需要设置以下请求头: +- User-Agent: 浏览器标识 +- Referer: 来源页面 +- Accept: 接受 JSON 格式 +- Accept-Language: 语言设置 + +#### 4.1.3 主要接口 + +**比赛数据接口**: +- 赛程和比分接口:获取指定日期的所有比赛 +- 比赛统计接口:获取比赛的详细统计数据 +- Play-by-Play 接口:获取比赛的回合数据 + +**统计数据接口**: +- 球队统计面板接口:获取球队的统计数据 +- 球队关键时刻数据接口:获取球队关键时刻的表现 +- 球员统计面板接口:获取球员的统计数据 +- 球员单场数据接口:获取球员的单场数据 + +#### 4.1.4 数据获取策略 + +**实时数据获取**: +- 比赛进行时:每 30 秒轮询一次比赛数据 +- 比赛未开始:每 5 分钟轮询一次(检查比赛状态) +- 比赛结束:每 10 分钟轮询一次(确保数据完整) + +**统计数据获取**: +- 每天更新一次球队和球员统计数据 +- 比赛结束后立即更新相关统计数据 + +### 4.2 Basketball Reference 爬虫集成 + +#### 4.2.1 数据来源 + +- **Base URL**: `https://www.basketball-reference.com` +- **数据格式**: HTML 页面 +- **优势**: 提供高级统计数据(PER、BPM、VORP、DRPM 等) + +#### 4.2.2 爬取策略 + +**请求频率控制**: +- 请求间隔:至少 1 秒(避免反爬虫) +- 请求队列:使用队列管理请求,控制并发数 + +**数据爬取**: +- 球员高级统计数据:从球员页面爬取 PER、BPM、VORP、DRPM 等 +- 球队高级统计数据:从球队页面爬取相关数据 +- 阵容统计数据:从阵容页面爬取阵容组合数据 + +**数据缓存**: +- 爬取的数据存储到数据库,避免重复请求 +- 高级统计数据更新频率较低,可以缓存较长时间 + +### 4.3 数据存储方案 + +#### 4.3.1 数据库设计 + +**策略配置表**: +- 存储用户配置的策略参数 +- 包含策略名称、配置参数、启用状态等字段 + +**比赛数据表**: +- 存储比赛基本信息(比赛 ID、日期、主客场、比分等) +- 存储比赛状态(未开始/进行中/已结束) +- 存储当前节次和剩余时间 + +**统计数据表**: +- 球队统计表:存储球队的统计数据 +- 球员统计表:存储球员的统计数据 +- Play-by-Play 表:存储比赛的回合数据 + +**交易信号表**: +- 存储生成的交易信号 +- 包含市场信息、方向、价格、数量、状态等字段 +- 包含信号的执行结果和统计信息 + +#### 4.3.2 缓存策略 + +**Redis 缓存**: +- 比赛数据缓存:缓存 5 分钟(实时数据) +- 统计数据缓存:缓存 1 小时(统计数据更新较慢) +- 高级统计缓存:缓存 24 小时(Basketball Reference 更新较慢) + +**缓存更新**: +- 数据更新时同步更新缓存 +- 缓存过期后自动从数据库或 API 重新获取 + +--- + +## 五、实时性保证策略 + +### 5.1 数据实时性要求 + +不同数据类型的实时性要求: + +| 数据类型 | 实时性要求 | 更新频率 | 延迟容忍度 | +|---------|-----------|---------|-----------| +| **比赛比分** | 极高 | 30秒 | < 1分钟 | +| **Play-by-Play** | 极高 | 30秒 | < 1分钟 | +| **比赛状态** | 高 | 30秒 | < 2分钟 | +| **统计数据** | 中 | 5分钟 | < 10分钟 | +| **历史数据** | 低 | 每天 | 无要求 | + +### 5.2 实时性保证方案 + +#### 5.2.1 多层级数据更新策略 + +**第一层:数据源层** +- HTTP 轮询:从 NBA Stats API 轮询获取数据 +- 第三方 WebSocket API:如果可用,使用 WebSocket 实时接收数据 +- 数据缓存:使用 Redis 缓存数据,减少 API 调用 + +**第二层:数据处理层** +- 数据变化检测:只处理变化的数据,减少不必要的处理 +- 数据验证:验证数据的有效性和完整性 +- 增量更新:只更新变化的数据,提高效率 + +**第三层:数据推送层** +- WebSocket 推送:通过 WebSocket 实时推送数据给前端 +- 订阅管理:管理前端的订阅,只推送订阅的数据 +- 批量推送:多个数据可以批量推送,减少网络开销 + +**第四层:前端展示层** +- 实时更新:前端实时接收和展示数据 +- 降级轮询:WebSocket 失败时降级到 HTTP 轮询 +- 数据缓存:前端缓存数据,减少重复请求 + +#### 5.2.2 数据变化检测机制 + +**数据快照比较**: +- 保存上次的数据快照 +- 比较新数据和快照,只处理变化的数据 +- 更新快照,用于下次比较 + +**增量更新**: +- 只保存和推送新增或变化的数据 +- 减少数据库写入和网络传输 +- 提高系统性能 + +#### 5.2.3 动态轮询频率调整 + +根据比赛状态动态调整轮询频率: + +- **比赛未开始**:5 分钟轮询一次 +- **比赛进行中**:30 秒轮询一次 +- **最后 5 分钟**:15 秒轮询一次(关键时刻) +- **加时赛**:20 秒轮询一次 +- **比赛结束**:10 分钟轮询一次(确保数据完整) + +#### 5.2.4 WebSocket 连接管理 + +**连接保活**: +- 定期发送心跳消息,保持连接活跃 +- 检测连接状态,及时处理断开情况 + +**自动重连**: +- 连接断开后自动重连 +- 使用指数退避策略,避免频繁重连 +- 重连后恢复订阅,确保数据不丢失 + +**连接监控**: +- 监控连接健康状态 +- 记录连接统计信息 +- 异常情况告警 + +#### 5.2.5 前端降级策略 + +**WebSocket 失败降级**: +- WebSocket 连接失败时,自动降级到 HTTP 轮询 +- 前端定期轮询获取数据,确保数据不中断 +- WebSocket 恢复后,自动切换回 WebSocket + +**数据缓存**: +- 前端缓存最近的数据 +- 网络中断时,使用缓存数据展示 +- 网络恢复后,同步最新数据 + +### 5.3 实时性监控 + +#### 5.3.1 数据延迟监控 + +**延迟统计**: +- 记录数据从获取到推送的延迟时间 +- 统计平均延迟、最大延迟等指标 +- 延迟超过阈值时触发告警 + +**性能指标**: +- API 调用响应时间 +- 数据处理时间 +- WebSocket 推送延迟 + +#### 5.3.2 告警机制 + +**告警条件**: +- 数据延迟超过阈值(如 1 分钟) +- API 调用失败率超过阈值 +- WebSocket 连接断开 +- 数据更新异常 + +**告警方式**: +- 日志记录 +- 系统通知 +- 邮件/短信通知(可选) + +--- + +## 六、量化分析逻辑设计 + +### 6.1 分析流程 + +#### 6.1.1 数据准备 + +**数据获取**: +- 获取实时比赛数据(比分、分差、剩余时间等) +- 获取统计数据(球队统计、球员统计等) +- 获取历史数据(历史比赛、历史统计等) + +**数据预处理**: +- 数据清洗:去除异常值和缺失值 +- 数据验证:验证数据的有效性和完整性 +- 数据标准化:将数据标准化为统一的格式 + +#### 6.1.2 特征提取 + +**基础特征**: +- 当前分差 +- 剩余时间 +- 当前节次 +- 比赛状态 + +**统计特征**: +- 球队实力指标(净效率值、攻防效率等) +- 球员表现指标(PER、TS%、USG% 等) +- 历史对战数据 + +**衍生特征**: +- 分差/剩余时间比 +- 球队实力差 +- 势头动量(过去 N 回合的净胜分) +- 球星爆发因子(本场表现 vs 赛季平均) + +#### 6.1.3 策略执行 + +**综合评分和对位分析**: +- 计算主队和客队的综合评分(基础实力、近期状态、阵容完整度、球星状态、环境因素) +- 分析对位优势(球星对位、阵容克制) +- 计算实时状态调整(分差、势头动量,仅比赛进行中) + +**获胜概率和交易价值计算**: +- 基于综合评分计算基础获胜概率 +- 根据对位优势和实时状态调整概率 +- 计算预期收益和风险调整收益 +- 计算交易价值评分 + +**触发条件判断**: +- 判断获胜概率差异是否达到阈值(如主队获胜概率 > 0.55 或 < 0.45) +- 判断交易价值评分是否达到阈值(如 > 0.05) +- 判断其他触发条件(剩余时间、价格等) + +**交易规则计算**: +- 根据获胜概率确定买入方向(YES 或 NO) +- 根据配置的交易规则,计算买入/卖出价格(固定价格/市场价格/动态价格) +- 根据配置的交易规则,计算买入/卖出数量(固定金额/按比例/动态计算) +- 根据配置的风险控制参数,验证交易是否合规 + +**信号生成**: +- 生成买入信号:包含市场 ID、方向(由系统自动判断)、价格、数量、原因(包含综合评分、获胜概率、交易价值等)等 +- 生成卖出信号:包含市场 ID、方向、价格、数量、原因等 +- 信号验证:验证信号的有效性和合规性 + +### 6.2 策略配置映射 + +#### 6.2.1 配置参数到分析逻辑的映射 + +**触发条件配置**: +- 比赛筛选 → 数据过滤逻辑 +- 数据指标 → 特征提取逻辑 +- 触发阈值 → 条件判断逻辑 + +**交易规则配置**: +- 买入规则 → 买入信号生成逻辑 +- 卖出规则 → 卖出信号生成逻辑 +- 价格策略 → 价格计算逻辑 + +**风险控制配置**: +- 最大/最小持仓 → 数量限制逻辑 +- 每日亏损限制 → 风险检查逻辑 +- 每日订单限制 → 订单计数逻辑 +- 价格容忍度 → 价格验证逻辑 + +### 6.3 信号生成规则 + +#### 6.3.1 买入信号生成 + +**触发条件**: +- 满足配置的买入触发条件 +- 通过风险控制检查 +- 市场状态正常(可交易) + +**信号内容**: +- 市场 ID:目标市场的唯一标识 +- 方向:YES 或 NO +- 价格:买入价格(根据价格策略计算) +- 数量:买入数量(根据交易规则计算) +- 原因:触发买入的原因(如"分差达到阈值") +- 时间戳:信号生成时间 + +#### 6.3.2 卖出信号生成 + +**触发条件**: +- 满足配置的卖出触发条件 +- 通过风险控制检查 +- 市场状态正常(可交易) + +**信号内容**: +- 市场 ID:目标市场的唯一标识 +- 方向:YES 或 NO +- 价格:卖出价格(根据价格策略计算) +- 数量:卖出数量(根据交易规则计算) +- 原因:触发卖出的原因(如"达到止盈条件") +- 时间戳:信号生成时间 + +--- + +## 七、WebSocket 推送方案 + +### 7.1 推送架构 + +#### 7.1.1 推送流程 + +``` +量化分析服务生成交易信号 + ↓ +信号推送服务接收信号 + ↓ +检查订阅关系(哪些前端订阅了该策略) + ↓ +通过 WebSocket 推送给订阅的前端 + ↓ +前端接收并展示信号 +``` + +#### 7.1.2 订阅管理 + +**订阅方式**: +- 前端通过 WebSocket 发送订阅消息 +- 支持订阅所有策略的信号 +- 支持订阅特定策略的信号 +- 支持订阅特定市场的信号 + +**订阅存储**: +- 后端维护订阅关系(策略 ID → 前端会话列表) +- 支持多个前端同时订阅同一策略 +- 支持前端动态订阅/取消订阅 + +**订阅恢复**: +- 前端连接断开后,自动清理订阅关系 +- 前端重连后,可以重新订阅 + +### 7.2 推送消息格式 + +#### 7.2.1 买入信号消息 + +包含以下字段: +- 消息类型:买入信号 +- 策略 ID:生成信号的策略标识 +- 市场 ID:目标市场标识 +- 方向:YES 或 NO +- 价格:买入价格 +- 数量:买入数量 +- 原因:触发原因 +- 时间戳:信号生成时间 + +#### 7.2.2 卖出信号消息 + +包含以下字段: +- 消息类型:卖出信号 +- 策略 ID:生成信号的策略标识 +- 市场 ID:目标市场标识 +- 方向:YES 或 NO +- 价格:卖出价格 +- 数量:卖出数量 +- 原因:触发原因 +- 时间戳:信号生成时间 + +#### 7.2.3 信号状态更新消息 + +包含以下字段: +- 消息类型:状态更新 +- 信号 ID:信号的唯一标识 +- 状态:执行中/执行成功/执行失败 +- 结果:执行结果详情 +- 时间戳:状态更新时间 + +### 7.3 推送优化 + +#### 7.3.1 批量推送 + +**批量策略**: +- 多个信号可以批量推送,减少网络开销 +- 批量大小可配置(如每批 10 个信号) +- 批量推送间隔可配置(如每 1 秒推送一次) + +**批量格式**: +- 单个消息包含多个信号 +- 前端解析后分别处理每个信号 + +#### 7.3.2 推送频率控制 + +**频率限制**: +- 每个策略的信号推送频率可配置 +- 避免过于频繁的推送,影响前端性能 +- 支持优先级:重要信号立即推送,普通信号批量推送 + +#### 7.3.3 推送失败处理 + +**失败重试**: +- 推送失败时,记录失败信息 +- 支持重试机制(如重试 3 次) +- 重试失败后,记录到数据库,后续可以查询 + +**降级策略**: +- WebSocket 推送失败时,可以降级到 HTTP 轮询 +- 前端定期轮询获取信号,确保不丢失 + +--- + +## 八、错误处理和容错机制 + +### 8.1 数据获取错误处理 + +#### 8.1.1 API 调用失败 + +**重试策略**: +- API 调用失败时,自动重试(如重试 3 次) +- 使用指数退避策略,避免频繁重试 +- 重试失败后,记录错误日志 + +**降级策略**: +- API 调用失败时,使用缓存数据 +- 缓存数据过期时,使用数据库数据 +- 所有数据源都失败时,记录错误并告警 + +#### 8.1.2 数据解析错误 + +**错误处理**: +- 数据格式异常时,记录错误日志 +- 跳过异常数据,继续处理其他数据 +- 数据缺失时,使用默认值或历史数据 + +### 8.2 量化分析错误处理 + +#### 8.2.1 分析逻辑错误 + +**错误捕获**: +- 分析逻辑执行时,捕获所有异常 +- 记录错误日志,包含错误详情和上下文 +- 错误不影响其他策略的执行 + +**错误恢复**: +- 错误发生后,跳过本次分析 +- 等待下次分析周期,重新执行 +- 连续错误时,触发告警 + +#### 8.2.2 信号生成错误 + +**错误处理**: +- 信号生成失败时,记录错误日志 +- 不生成无效信号,避免误导用户 +- 错误原因推送给前端(可选) + +### 8.3 推送错误处理 + +#### 8.3.1 WebSocket 推送失败 + +**失败处理**: +- 推送失败时,记录失败信息 +- 支持重试机制(如重试 3 次) +- 重试失败后,记录到数据库 + +**降级策略**: +- WebSocket 推送失败时,可以降级到 HTTP 轮询 +- 前端定期轮询获取信号,确保不丢失 + +#### 8.3.2 前端连接断开 + +**连接管理**: +- 检测前端连接断开,清理订阅关系 +- 前端重连后,可以重新订阅 +- 连接断开期间,信号暂存到数据库 + +--- + +## 九、性能优化方案 + +### 9.1 数据获取优化 + +#### 9.1.1 请求频率控制 + +**频率限制**: +- 控制 API 请求频率,避免触发速率限制 +- 使用请求队列,管理并发请求 +- 请求失败时,使用指数退避策略 + +#### 9.1.2 数据缓存 + +**缓存策略**: +- 实时数据缓存 5 分钟 +- 统计数据缓存 1 小时 +- 高级统计缓存 24 小时 + +**缓存更新**: +- 数据更新时同步更新缓存 +- 缓存过期后自动刷新 + +### 9.2 量化分析优化 + +#### 9.2.1 增量分析 + +**变化检测**: +- 只分析变化的数据,减少不必要的计算 +- 使用数据快照比较,检测数据变化 +- 只处理变化的数据,提高分析效率 + +#### 9.2.2 并行分析 + +**并行策略**: +- 多个策略可以并行分析 +- 使用线程池管理分析任务 +- 避免资源竞争,确保分析准确性 + +### 9.3 推送优化 + +#### 9.3.1 批量推送 + +**批量策略**: +- 多个信号批量推送,减少网络开销 +- 批量大小和间隔可配置 +- 重要信号可以立即推送 + +#### 9.3.2 推送频率控制 + +**频率限制**: +- 控制推送频率,避免前端性能问题 +- 支持优先级:重要信号立即推送 +- 普通信号批量推送 + +--- + +## 十、监控和日志 + +### 10.1 系统监控 + +#### 10.1.1 性能监控 + +**监控指标**: +- API 调用响应时间 +- 数据处理时间 +- WebSocket 推送延迟 +- 系统资源使用率(CPU、内存等) + +**监控方式**: +- 定期记录性能指标 +- 性能指标超过阈值时触发告警 +- 提供性能统计报表 + +#### 10.1.2 业务监控 + +**监控指标**: +- 策略执行次数 +- 信号生成数量 +- 信号推送成功率 +- 数据更新延迟 + +**监控方式**: +- 记录业务指标到数据库 +- 提供业务统计报表 +- 异常情况触发告警 + +### 10.2 日志管理 + +#### 10.2.1 日志级别 + +**日志分类**: +- 错误日志:记录系统错误和异常 +- 警告日志:记录警告信息 +- 信息日志:记录关键操作信息 +- 调试日志:记录详细调试信息 + +#### 10.2.2 日志内容 + +**关键操作日志**: +- 策略配置变更 +- 量化分析执行 +- 信号生成和推送 +- API 调用和错误 + +**日志格式**: +- 统一日志格式,便于解析和分析 +- 包含时间戳、日志级别、模块、消息等字段 +- 支持结构化日志(JSON 格式) + +--- + +## 十一、实施步骤 + +### 11.1 第一阶段:基础功能开发(2-3 周) + +**任务清单**: +- 完成策略配置管理功能(前端配置页面 + 后端 API) +- 完成 NBA 数据获取功能(API 集成 + 数据存储) +- 完成基础量化分析功能(简单策略逻辑) +- 完成信号推送功能(WebSocket 推送) + +### 11.2 第二阶段:量化分析完善(2-3 周) + +**任务清单**: +- 完善量化分析逻辑(支持复杂策略) +- 完善特征提取和数据处理 +- 完善信号生成和验证 +- 完善错误处理和容错机制 + +### 11.3 第三阶段:实时性优化(1-2 周) + +**任务清单**: +- 优化数据获取频率和策略 +- 优化 WebSocket 推送机制 +- 实现数据变化检测和增量更新 +- 实现前端降级策略 + +### 11.4 第四阶段:性能优化和监控(1-2 周) + +**任务清单**: +- 性能优化(缓存、并行处理等) +- 系统监控和日志完善 +- 压力测试和性能调优 +- 文档完善 + +--- + +## 十二、注意事项 + +### 12.1 API 限制 + +- **NBA Stats API**: 建议请求频率 < 10 请求/秒 +- **Basketball Reference**: 建议请求频率 < 1 请求/秒 +- 需要设置正确的请求头,避免被拒绝 + +### 12.2 数据质量 + +- API 返回的数据格式可能不一致,需要做好异常处理 +- Basketball Reference 的 HTML 结构可能变化,需要定期检查 +- 需要验证数据的合理性(如得分不超过 200 分) + +### 12.3 法律合规 + +- 遵守 NBA Stats API 的使用条款 +- 遵守 Basketball Reference 的爬虫协议(robots.txt) +- 不要过度请求,避免对服务器造成负担 + +### 12.4 风险提示 + +- 量化交易存在风险,需要做好风险控制 +- 信号仅供参考,不构成投资建议 +- 需要明确告知用户风险和使用条款 + +--- + +## 十三、扩展建议 + +### 13.1 功能扩展 + +**策略模板**: +- 提供预定义的策略模板 +- 用户可以基于模板快速创建策略 +- 支持策略模板的导入和导出 + +**回测功能**: +- 支持历史数据回测 +- 评估策略的历史表现 +- 优化策略参数 + +**多账户支持**: +- 支持多个交易账户 +- 每个账户可以配置不同的策略 +- 统一管理和监控 + +### 13.2 技术扩展 + +**机器学习集成**: +- 使用机器学习模型优化策略 +- 自动学习和优化参数 +- 提高信号准确性 + +**多数据源整合**: +- 整合多个数据源(如 ESPN、Sportradar 等) +- 提高数据完整性和准确性 +- 降低对单一数据源的依赖 + +**分布式部署**: +- 支持分布式部署,提高系统性能 +- 支持负载均衡和容错 +- 支持水平扩展 diff --git a/docs/zh/nba-api-verification-checklist.md b/docs/zh/nba-api-verification-checklist.md new file mode 100644 index 0000000..b5e0e0a --- /dev/null +++ b/docs/zh/nba-api-verification-checklist.md @@ -0,0 +1,158 @@ +# NBA Stats API 验证检查清单 + +## 一、API 接口定义检查 + +### 1.1 接口路径 +- ✅ **路径**: `/Scoreboard` +- ✅ **Base URL**: `https://stats.nba.com/stats/` +- ✅ **完整 URL**: `https://stats.nba.com/stats/Scoreboard` + +### 1.2 请求参数 +- ✅ **GameDate**: 日期格式 `YYYY-MM-DD`(如 `2024-12-15`) +- ✅ **LeagueID**: 联盟ID,默认 `"00"` (NBA) +- ✅ **DayOffset**: 日期偏移,默认 `0` + +**注意**: NBA Stats API 的参数名称是**大小写敏感**的: +- ✅ `GameDate` (正确) +- ❌ `gameDate` (错误) +- ✅ `LeagueID` (正确) +- ❌ `leagueId` (错误) + +### 1.3 请求头设置 +- ✅ **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` +- ✅ **Referer**: `https://www.nba.com/` +- ✅ **Accept**: `application/json` +- ✅ **Accept-Language**: `en-US,en;q=0.9` +- ✅ **Origin**: `https://www.nba.com` + +## 二、响应格式检查 + +### 2.1 响应结构 +NBA Stats API 返回的 JSON 结构: +```json +{ + "resultSets": [ + { + "name": "GameHeader", + "headers": ["GAME_DATE_EST", "GAME_SEQUENCE", "GAME_ID", ...], + "rowSet": [[...], [...]] + }, + { + "name": "LineScore", + "headers": ["GAME_DATE_EST", "GAME_SEQUENCE", "GAME_ID", "TEAM_ID", ...], + "rowSet": [[...], [...]] + } + ] +} +``` + +### 2.2 ResultSet 名称 +- ✅ **GameHeader**: 比赛基本信息 +- ✅ **LineScore**: 比分信息(每支球队一行) + +### 2.3 GameHeader 字段顺序 +根据注释,字段顺序应该是: +``` +[0] GAME_DATE_EST +[1] GAME_SEQUENCE +[2] GAME_ID +[3] GAME_STATUS_ID +[4] GAME_STATUS_TEXT +[5] GAMECODE +[6] HOME_TEAM_ID +[7] VISITOR_TEAM_ID +[8] SEASON +[9] LIVE_PERIOD +[10] LIVE_PC_TIME +[11] NATL_TV_BROADCASTER_ABBREV +[12] LIVE_PERIOD_TIME_BCAST +[13] WH_STATUS +``` + +### 2.4 LineScore 字段顺序 +根据注释,字段顺序应该是: +``` +[0] GAME_DATE_EST +[1] GAME_SEQUENCE +[2] GAME_ID +[3] TEAM_ID +[4] TEAM_ABBREVIATION +[5] TEAM_NAME +[6] PTS_QTR1 +[7] PTS_QTR2 +[8] PTS_QTR3 +[9] PTS_QTR4 +[10] PTS_OT1 +[11] PTS_OT2 +[12] PTS_OT3 +[13] PTS_OT4 +[14] PTS +[15] FG_PCT +[16] FT_PCT +[17] FG3_PCT +[18] AST +[19] REB +[20] TOV +``` + +## 三、代码实现检查 + +### 3.1 接口定义 ✅ +```kotlin +@GET("Scoreboard") +suspend fun getScoreboard( + @Query("GameDate") gameDate: String? = null, + @Query("LeagueID") leagueId: String = "00", + @Query("DayOffset") dayOffset: Int = 0 +): Response +``` +- ✅ 参数名称大小写正确 +- ✅ 参数类型正确 + +### 3.2 请求头设置 ✅ +- ✅ 已设置所有必需的请求头 +- ✅ User-Agent 格式正确 + +### 3.3 响应解析 ⚠️ +**潜在问题**: +1. **ResultSet 查找方式**: 使用 `firstOrNull { it.name == "GameHeader" }` 可能不够准确 + - 建议:使用索引 `resultSets[0]` 或 `resultSets.getOrNull(0)` + - 或者:先检查 `resultSets.size >= 2` + +2. **字段索引**: 当前代码假设字段顺序固定 + - 建议:根据 `headers` 数组动态查找字段索引,而不是硬编码索引 + +3. **错误处理**: 当前有基本的错误处理,但可以更详细 + +## 四、建议的改进 + +### 4.1 使用 headers 动态查找字段 +```kotlin +// 根据 headers 查找字段索引,而不是硬编码 +val gameDateIndex = headers.indexOf("GAME_DATE_EST") +val gameIdIndex = headers.indexOf("GAME_ID") +// ... +``` + +### 4.2 增强错误处理 +- 记录完整的响应内容(用于调试) +- 验证 headers 数量是否匹配 +- 验证 rowSet 数据是否完整 + +### 4.3 添加响应验证 +- 验证 resultSets 数量 +- 验证每个 resultSet 的 name +- 验证 headers 和 rowSet 的对应关系 + +## 五、测试建议 + +1. **单元测试**: 测试 API 调用和响应解析 +2. **集成测试**: 测试完整的获取流程 +3. **错误场景测试**: 测试 API 失败、数据不完整等情况 + +## 六、已知问题 + +1. **API 限制**: NBA Stats API 可能需要特定的请求头,否则可能返回 403 或空数据 +2. **数据格式**: 响应格式可能因日期而异(有比赛 vs 无比赛) +3. **时区问题**: `GAME_DATE_EST` 是 EST 时区,需要注意时区转换 + diff --git a/docs/zh/nba-backend-implementation-summary.md b/docs/zh/nba-backend-implementation-summary.md new file mode 100644 index 0000000..b496c79 --- /dev/null +++ b/docs/zh/nba-backend-implementation-summary.md @@ -0,0 +1,218 @@ +# NBA 量化交易系统后端实现总结 + +## 已完成部分 + +### 1. 数据库设计 ✅ + +**迁移文件**: `V11__create_nba_quantitative_trading_tables.sql` + +创建了以下表: +- `nba_markets`: NBA 市场表(Polymarket 市场信息) +- `nba_games`: NBA 比赛表 +- `nba_quantitative_strategies`: 量化策略配置表 +- `nba_trading_signals`: 交易信号表 +- `nba_strategy_statistics`: 策略执行统计表 + +### 2. 实体类 ✅ + +已创建以下实体类: +- `NbaMarket.kt`: NBA 市场实体 +- `NbaGame.kt`: NBA 比赛实体 +- `NbaQuantitativeStrategy.kt`: 量化策略配置实体 +- `NbaTradingSignal.kt`: 交易信号实体 +- `NbaStrategyStatistics.kt`: 策略统计实体 + +### 3. Repository 层 ✅ + +已创建以下 Repository: +- `NbaMarketRepository.kt` +- `NbaGameRepository.kt` +- `NbaQuantitativeStrategyRepository.kt` +- `NbaTradingSignalRepository.kt` +- `NbaStrategyStatisticsRepository.kt` + +### 4. DTO 层 ✅ + +已创建: +- `NbaQuantitativeStrategyDto.kt`: 包含创建、更新、列表请求和响应 DTO + +### 5. Service 层(部分完成)✅ + +已创建: +- `NbaQuantitativeStrategyService.kt`: 策略管理服务 + - 创建策略 + - 更新策略 + - 获取策略列表 + - 获取策略详情 + - 删除策略 + - 获取启用的策略列表 + +### 6. Controller 层(部分完成)✅ + +已创建: +- `NbaQuantitativeStrategyController.kt`: 策略管理 API + - `POST /api/nba/strategies/create`: 创建策略 + - `POST /api/nba/strategies/update`: 更新策略 + - `POST /api/nba/strategies/list`: 获取策略列表 + - `POST /api/nba/strategies/detail`: 获取策略详情 + - `POST /api/nba/strategies/delete`: 删除策略 + +--- + +## 待完成部分 + +### 1. NBA 市场数据获取服务 ⏳ + +**需要创建**: +- `NbaMarketService.kt`: + - 从 Polymarket API 获取市场列表 + - 同步市场数据到数据库 + - 根据条件查询市场 + - 匹配 NBA 比赛和市场 + +**参考文档**: `docs/zh/polymarket-nba-markets-fetching-solution.md` + +### 2. NBA 比赛数据获取服务 ⏳ + +**需要创建**: +- `NbaGameService.kt`: + - 从 NBA API 获取比赛数据 + - 同步比赛数据到数据库 + - 实时更新比赛状态 + - 匹配比赛和市场 + +**需要集成**: NBA Stats API 或第三方 NBA 数据 API + +### 3. 量化分析服务 ⏳ + +**需要创建**: +- `NbaQuantitativeAnalysisService.kt`: + - 综合评分计算 + - 对位分析 + - 实时状态分析 + - 获胜概率计算 + - 交易价值计算 + +**参考文档**: `docs/zh/nba-quantitative-strategy-algorithm.md` + +### 4. 交易信号生成服务 ⏳ + +**需要创建**: +- `NbaTradingSignalService.kt`: + - 生成买入信号 + - 生成卖出信号 + - 风险控制检查 + - 信号验证 + +### 5. WebSocket 推送服务 ⏳ + +**需要创建**: +- `NbaTradingSignalPushService.kt`: + - WebSocket 连接管理 + - 信号推送 + - 订阅管理 + +**参考**: 现有的 `OrderPushService.kt` + +### 6. 定时任务和同步服务 ⏳ + +**需要创建**: +- `NbaMarketSyncScheduler.kt`: 同步 NBA 市场数据 +- `NbaGameSyncScheduler.kt`: 同步 NBA 比赛数据 +- `NbaQuantitativeAnalysisScheduler.kt`: 定时执行量化分析 + +### 7. 其他 Controller ⏳ + +**需要创建**: +- `NbaMarketController.kt`: 市场数据 API +- `NbaGameController.kt`: 比赛数据 API +- `NbaTradingSignalController.kt`: 交易信号 API +- `NbaStatisticsController.kt`: 统计 API + +### 8. 其他 DTO ⏳ + +**需要创建**: +- `NbaMarketDto.kt` +- `NbaGameDto.kt` +- `NbaTradingSignalDto.kt` +- `NbaStatisticsDto.kt` + +--- + +## 实现优先级 + +### 第一阶段(核心功能) +1. ✅ 数据库设计和实体类 +2. ✅ 策略管理服务(CRUD) +3. ⏳ NBA 市场数据获取服务 +4. ⏳ NBA 比赛数据获取服务 + +### 第二阶段(量化分析) +5. ⏳ 量化分析服务 +6. ⏳ 交易信号生成服务 +7. ⏳ 风险控制逻辑 + +### 第三阶段(实时推送) +8. ⏳ WebSocket 推送服务 +9. ⏳ 定时任务和同步服务 + +### 第四阶段(完善功能) +10. ⏳ 统计服务 +11. ⏳ 其他 API 接口 +12. ⏳ 错误处理和日志 + +--- + +## 技术要点 + +### 1. 数据同步策略 +- 市场数据:每天全量同步,每小时增量同步 +- 比赛数据:实时更新(比赛进行中) +- 分析频率:根据策略配置(默认 30 秒) + +### 2. 量化分析算法 +- 综合评分计算(基础实力、近期状态、阵容完整度等) +- 对位分析(球星对位、阵容克制) +- 实时状态分析(分差、势头) +- 获胜概率计算 +- 交易价值计算 + +### 3. 风险控制 +- 持仓限制检查 +- 每日限制检查 +- 价格容忍度检查 +- 概率置信度检查 + +### 4. WebSocket 推送 +- 实时推送交易信号 +- 支持订阅/取消订阅 +- 连接管理和重连机制 + +--- + +## 下一步工作 + +1. **实现 NBA 市场数据获取服务** + - 参考 `polymarket-nba-markets-fetching-solution.md` + - 实现批量查询和过滤逻辑 + +2. **实现 NBA 比赛数据获取服务** + - 集成 NBA Stats API + - 实现比赛数据同步和更新 + +3. **实现量化分析服务** + - 参考 `nba-quantitative-strategy-algorithm.md` + - 实现综合评分、对位分析、概率计算等核心算法 + +4. **实现交易信号生成服务** + - 集成量化分析结果 + - 实现买入/卖出信号生成逻辑 + +5. **实现 WebSocket 推送服务** + - 参考现有的 `OrderPushService` + - 实现信号推送和订阅管理 + +--- + +**文档结束** + diff --git a/docs/zh/nba-game-data-sources-comparison.md b/docs/zh/nba-game-data-sources-comparison.md new file mode 100644 index 0000000..e5783f1 --- /dev/null +++ b/docs/zh/nba-game-data-sources-comparison.md @@ -0,0 +1,100 @@ +# NBA 比赛数据来源对比 + +## 一、数据来源对比 + +### 1.1 NBA Stats API + +**优势**: +- ✅ 官方数据源,数据准确可靠 +- ✅ 包含完整的比赛数据(实时比分、状态、统计等) +- ✅ 实时更新,数据最新 +- ✅ 免费使用 +- ✅ 支持历史数据查询 + +**劣势**: +- ❌ 不包含 Polymarket 市场信息 +- ❌ 需要额外的匹配逻辑来关联 Polymarket 市场 + +**适用场景**: +- 获取比赛基本信息(球队、日期、时间) +- 获取实时比分和比赛状态 +- 获取比赛统计数据 + +### 1.2 Polymarket API + +**优势**: +- ✅ 直接获取交易市场信息 +- ✅ 包含市场 ID、价格、流动性等交易相关数据 +- ✅ 市场名称可能包含比赛信息 + +**劣势**: +- ❌ 不包含实时比分和比赛状态 +- ❌ 无法直接搜索或筛选 NBA 市场(需要知道 condition_ids) +- ❌ 市场名称格式不统一,解析困难 +- ❌ 数据不完整(缺少比赛详细信息) + +**适用场景**: +- 获取可交易的市场列表 +- 获取市场价格和流动性信息 +- 匹配比赛和交易市场 + +## 二、推荐方案 + +### 2.1 混合方案(推荐) + +**策略**:结合两种数据源,发挥各自优势 + +**实现方式**: +1. **主要数据源:NBA Stats API** + - 获取完整的比赛数据(球队、日期、时间、比分、状态等) + - 实时更新比赛状态 + +2. **补充数据源:Polymarket API** + - 获取市场信息(市场 ID、价格、流动性等) + - 通过球队名称和日期匹配比赛和市场 + +3. **数据匹配** + - 使用球队名称和比赛日期匹配 + - 建立比赛和市场的关联关系 + +### 2.2 数据流程 + +``` +1. 从 NBA Stats API 获取比赛列表 + ↓ +2. 从 Polymarket API 获取市场列表(如果知道 condition_ids) + ↓ +3. 解析市场名称,提取球队和日期信息 + ↓ +4. 匹配比赛和市场(通过球队名称和日期) + ↓ +5. 合并数据,返回完整的比赛和市场信息 +``` + +## 三、当前实现 + +当前系统使用 **NBA Stats API** 作为主要数据源,原因: +- 数据完整可靠 +- 实时更新 +- 免费使用 +- 无需预先知道 condition_ids + +如果需要 Polymarket 市场信息,可以: +1. 在获取比赛数据后,通过球队名称和日期匹配 Polymarket 市场 +2. 或者单独维护一个 Polymarket 市场列表,定期同步 + +## 四、建议 + +**对于获取比赛数据**: +- ✅ 使用 NBA Stats API(当前实现) +- ✅ 实时、准确、完整 + +**对于获取市场信息**: +- ✅ 使用 Polymarket API(需要知道 condition_ids) +- ✅ 或者从市场名称解析(如果格式统一) + +**最佳实践**: +- 主要使用 NBA Stats API 获取比赛数据 +- 使用 Polymarket API 获取市场信息并匹配 +- 建立比赛和市场的关联关系 + diff --git a/docs/zh/nba-hybrid-data-source-implementation.md b/docs/zh/nba-hybrid-data-source-implementation.md new file mode 100644 index 0000000..3113679 --- /dev/null +++ b/docs/zh/nba-hybrid-data-source-implementation.md @@ -0,0 +1,150 @@ +# NBA 比赛数据获取实现 + +## 一、概述 + +本系统从 NBA Stats API 实时获取 NBA 比赛数据: +- **数据源**:NBA Stats API(官方数据源,实时、准确、完整) +- **不依赖数据库**:所有数据从 API 实时获取 + +## 二、实现架构 + +### 2.1 数据流程 + +``` +1. 从 NBA Stats API 获取比赛列表(实时) + ↓ +2. 解析 API 响应,转换为 DTO + ↓ +3. 返回比赛数据给前端 +``` + +### 2.2 关于 Polymarket 市场匹配 + +**当前实现**: +- 不进行市场匹配(因为 Polymarket API 限制) +- 比赛数据中的 `polymarketMarketId` 字段为 `null` + +**未来扩展**: +- 如果将来有办法获取 condition_ids,可以从 Polymarket API 实时获取市场信息 +- 已保留 `NbaMarketNameParser` 工具类,可用于市场名称解析 + +### 2.2 核心组件 + +#### 1. NbaGameService +- **功能**:从 NBA Stats API 实时获取比赛数据 +- **数据源**:NBA Stats API(官方数据源) +- **特点**: + - 实时获取,不依赖数据库 + - 支持日期范围查询 + - 支持按状态过滤 + +#### 2. NbaMarketNameParser(保留,供将来使用) +- **功能**:解析 Polymarket 市场名称,提取球队和日期信息 +- **支持格式**: + - "Team1 vs Team2" + - "Team1 @ Team2" + - "Will Team1 beat Team2" + - "Team1 win" +- **日期格式**: + - "Dec 15, 2024" + - "2024-12-15" + - "12/15/2024" +- **说明**:当前未使用,保留以备将来扩展 + +## 三、实现细节 + +### 3.1 比赛数据获取 + +**获取流程**: +1. 根据日期范围,每天调用一次 NBA Stats API +2. 解析 Scoreboard 响应,提取比赛信息 +3. 组合 GameHeader 和 LineScore 数据 +4. 转换为 NbaGameDto 返回 + +**数据字段**: +- 比赛基本信息(球队、日期、时间) +- 实时比分和状态 +- 比赛节次和剩余时间 +- 球队统计信息 + +### 3.2 关于市场匹配(未来扩展) + +**当前状态**: +- 不进行市场匹配 +- `polymarketMarketId` 字段为 `null` + +**未来扩展方案**: +如果将来需要匹配市场,可以考虑: +1. 从其他来源获取 condition_ids(如爬取、手动维护等) +2. 使用 Polymarket API 实时查询这些市场 +3. 使用 `NbaMarketNameParser` 解析市场名称并匹配 + +### 3.3 数据返回 + +**NbaGameDto** 包含: +- 比赛基本信息(从 NBA Stats API) +- `polymarketMarketId`:当前为 `null`(未来可扩展) + +## 四、使用方式 + +### 4.1 API 调用 + +```kotlin +// 获取比赛列表(自动匹配市场) +val request = NbaGameListRequest( + startDate = "2024-12-15", + endDate = "2024-12-22" +) +val result = nbaGameService.getNbaGames(request) +``` + +### 4.2 返回数据 + +```kotlin +data class NbaGameDto( + val nbaGameId: String?, + val homeTeam: String, + val awayTeam: String, + val gameDate: LocalDate, + val gameStatus: String, + val homeScore: Int, + val awayScore: Int, + val polymarketMarketId: String? // 匹配的市场 ID +) +``` + +## 五、优势 + +1. **数据完整**:从 NBA Stats API 获取完整的比赛数据 +2. **实时更新**:比赛数据实时获取,不依赖数据库 +3. **简单高效**:直接调用 API,无需维护数据库 +4. **官方数据源**:数据准确可靠 + +## 六、注意事项 + +1. **API 限制**: + - NBA Stats API 需要设置正确的请求头 + - 建议控制请求频率(< 10 请求/秒) + +2. **市场匹配**: + - 当前不进行市场匹配 + - 如果需要匹配,需要解决 Polymarket API 的限制(需要知道 condition_ids) + +3. **性能考虑**: + - 日期范围查询会多次调用 API(每天一次) + - 建议合理设置日期范围,避免查询过长的时间段 + +## 七、后续优化 + +1. **缓存机制**: + - 缓存 API 响应,减少重复请求 + - 设置合理的缓存时间(如 30 秒) + +2. **错误处理**: + - 实现重试机制(指数退避) + - 处理 API 临时不可用的情况 + +3. **市场匹配(可选)**: + - 如果将来有办法获取 condition_ids,可以实现市场匹配 + - 使用 `NbaMarketNameParser` 进行市场名称解析 + diff --git a/docs/zh/nba-quantitative-strategy-algorithm.md b/docs/zh/nba-quantitative-strategy-algorithm.md new file mode 100644 index 0000000..42dea93 --- /dev/null +++ b/docs/zh/nba-quantitative-strategy-algorithm.md @@ -0,0 +1,834 @@ +# NBA 量化交易策略算法文档 + +## 一、算法概述 + +### 1.1 算法目标 + +本算法基于 NBA 比赛数据,通过量化分析两支队伍的实力、状态、对位关系等因素,计算出哪支队伍具有更高的获胜概率和交易价值,从而生成买入/卖出交易信号。 + +### 1.2 算法流程 + +``` +获取比赛数据(主队 vs 客队) + ↓ +数据预处理和特征提取 + ↓ +计算两队综合评分 + ↓ +计算获胜概率和预期收益 + ↓ +生成交易信号(买入/卖出) + ↓ +风险控制检查 + ↓ +输出交易信号 +``` + +### 1.3 核心思想 + +**不是简单的"主队落后买入",而是**: +- 综合分析两支队伍的各项指标 +- 计算每支队伍的相对优势和劣势 +- 评估交易价值和风险 +- 选择最优的交易方向和时机 + +--- + +## 二、数据预处理 + +### 2.1 基础数据获取 + +**比赛基本信息**: +- 比赛 ID、日期、主客场 +- 当前比分、分差、节次、剩余时间 +- 比赛状态(未开始/进行中/已结束) + +**球队基础数据**: +- 主队和客队的基本信息 +- 当前赛季的统计数据 +- 近期表现数据(近 5 场/10 场) + +**球员数据**: +- 核心球员的统计数据 +- 球员健康状况和轮休情况 +- 球员近期表现 + +### 2.2 数据清洗 + +**异常值处理**: +- 检查数据合理性(如得分不超过 200 分) +- 处理缺失值(使用历史平均值或默认值) +- 处理异常数据(如负分差、超时时间等) + +**数据标准化**: +- 将不同量纲的数据标准化到统一范围 +- 使用 Z-score 或 Min-Max 归一化 +- 确保数据在 [0, 1] 或 [-1, 1] 范围内 + +### 2.3 特征工程 + +**基础特征**: +- 当前分差(主队得分 - 客队得分) +- 剩余时间(分钟) +- 当前节次(1-4,加时) +- 比赛状态 + +**统计特征**: +- 球队净效率值(进攻效率 - 防守效率) +- 球队比赛节奏(Pace) +- 球队关键时刻表现(Clutch 数据) +- 球队主客场表现差异 + +**动态特征**: +- 势头动量(过去 N 回合的净胜分) +- 手感热度(近期命中率) +- 阵容对位优势 +- 球星爆发因子 + +--- + +## 三、综合评分算法 + +### 3.1 球队实力评分 + +#### 3.1.1 基础实力评分 + +**计算公式**: +``` +基础实力评分 = 净效率值评分 × W1 + 攻防效率评分 × W2 + 节奏评分 × W3 +``` + +**评分组成**: +- **净效率值评分**:基于球队赛季平均净效率值,归一化到 [0, 1] + - 评分 = (净效率值 - 联盟最低值) / (联盟最高值 - 联盟最低值) +- **攻防效率评分**:综合考虑进攻效率和防守效率 + - 进攻效率评分 = (进攻效率排名 - 1) / (总球队数 - 1),排名越靠前评分越高 + - 防守效率评分 = (防守效率排名 - 1) / (总球队数 - 1),排名越靠前评分越高 + - 攻防效率评分 = (进攻效率评分 + 防守效率评分) / 2 +- **节奏评分**:基于比赛节奏(Pace) + - 节奏评分 = (Pace - 联盟最低值) / (联盟最高值 - 联盟最低值) + +**权重设置**: +- W1 = 0.5(净效率值权重最高) +- W2 = 0.3(攻防效率权重) +- W3 = 0.2(节奏权重) + +#### 3.1.2 近期状态评分 + +**计算公式**: +``` +近期状态评分 = 近期胜率评分 × W1 + 近期净效率值变化 × W2 + 势头评分 × W3 +``` + +**评分组成**: +- **近期胜率评分**:近 5 场或 10 场胜率 + - 评分 = 近期胜率(0-1) +- **近期净效率值变化**:近期净效率值与赛季平均的差值 + - 变化 = (近期净效率值 - 赛季平均净效率值) / 赛季平均净效率值 + - 评分 = (变化 + 1) / 2(归一化到 [0, 1]) +- **势头评分**:基于连胜/连败场次 + - 连胜:评分 = min(连胜场次 / 5, 1) + - 连败:评分 = max(1 - 连败场次 / 5, 0) + +**权重设置**: +- W1 = 0.4(胜率权重) +- W2 = 0.4(净效率值变化权重) +- W3 = 0.2(势头权重) + +#### 3.1.3 阵容完整度评分 + +**计算公式**: +``` +阵容完整度评分 = 1 - (缺失球员总VORP / 球队总VORP) +``` + +**评分说明**: +- 如果所有核心球员都在,评分为 1.0 +- 如果有核心球员缺席,根据缺失球员的 VORP 值降低评分 +- 缺失球员越多、重要性越高,评分越低 + +#### 3.1.4 球星状态评分 + +**计算公式**: +``` +球星状态评分 = 核心球星PER评分 × W1 + 核心球星TS%评分 × W2 + 核心球星健康评分 × W3 +``` + +**评分组成**: +- **核心球星 PER 评分**:基于近期 PER 与赛季平均的对比 + - 评分 = (近期PER - 赛季平均PER) / 赛季平均PER + 1,归一化到 [0, 1] +- **核心球星 TS% 评分**:基于近期真实命中率 + - 评分 = (近期TS% - 赛季平均TS%) / 赛季平均TS% + 1,归一化到 [0, 1] +- **核心球星健康评分**: + - 健康:1.0 + - 出战成疑:0.7 + - 缺席:0.3 + +**权重设置**: +- W1 = 0.4(PER 权重) +- W2 = 0.4(TS% 权重) +- W3 = 0.2(健康权重) + +#### 3.1.5 环境因素评分 + +**计算公式**: +``` +环境因素评分 = 主客场优势评分 × W1 + 休息天数评分 × W2 + 背靠背影响评分 × W3 +``` + +**评分组成**: +- **主客场优势评分**: + - 主场:1.0 + - 客场:基于客场胜率,评分 = 客场胜率 / 主场胜率 +- **休息天数评分**: + - 休息 1 天:0.9 + - 休息 2 天:1.0(最优) + - 休息 3 天以上:0.95 +- **背靠背影响评分**: + - 非背靠背:1.0 + - 背靠背第二场:0.85 + +**权重设置**: +- W1 = 0.5(主客场权重最高) +- W2 = 0.3(休息天数权重) +- W3 = 0.2(背靠背权重) + +### 3.2 综合评分计算 + +**主队综合评分**: +``` +主队综合评分 = 基础实力评分 × 0.3 + + 近期状态评分 × 0.25 + + 阵容完整度评分 × 0.2 + + 球星状态评分 × 0.15 + + 环境因素评分 × 0.1 +``` + +**客队综合评分**: +``` +客队综合评分 = 基础实力评分 × 0.3 + + 近期状态评分 × 0.25 + + 阵容完整度评分 × 0.2 + + 球星状态评分 × 0.15 + + 环境因素评分 × 0.1 +``` + +**相对优势评分**: +``` +主队相对优势 = 主队综合评分 - 客队综合评分 +``` + +--- + +## 四、对位分析算法 + +### 4.1 球星对位分析 + +**对位优势评分**: +``` +对位优势评分 = 防守限制效果评分 × W1 + 历史对位数据评分 × W2 +``` + +**评分组成**: +- **防守限制效果评分**:基于防守球员的 DRPM 和进攻球员的 ORPM + - 如果防守球员 DRPM 高,且进攻球员 ORPM 高,则限制效果好 + - 评分 = (防守球员DRPM - 联盟平均DRPM) / (联盟最高DRPM - 联盟平均DRPM) +- **历史对位数据评分**:基于历史对位时的表现 + - 对位效率差 = (对位时进攻球员TS% - 赛季平均TS%) + - 评分 = 1 - (对位效率差 / 最大可能效率差),归一化到 [0, 1] + +**权重设置**: +- W1 = 0.6(防守限制效果权重) +- W2 = 0.4(历史对位数据权重) + +### 4.2 阵容克制分析 + +**阵容克制评分**: +``` +阵容克制评分 = 内线优势评分 × W1 + 外线优势评分 × W2 + 快攻优势评分 × W3 +``` + +**评分组成**: +- **内线优势评分**: + - 如果主队内线得分占比高,且客队内线防守效率低,则主队有优势 + - 评分 = (主队内线得分占比 - 客队内线防守效率排名归一化) / 2 +- **外线优势评分**: + - 如果主队三分出手占比高,且客队三分防守效率低,则主队有优势 + - 评分 = (主队三分出手占比 - 客队三分防守效率排名归一化) / 2 +- **快攻优势评分**: + - 如果主队快攻得分占比高,且客队快攻防守效率低,则主队有优势 + - 评分 = (主队快攻得分占比 - 客队快攻防守效率排名归一化) / 2 + +**权重设置**: +- W1 = 0.4(内线权重) +- W2 = 0.4(外线权重) +- W3 = 0.2(快攻权重) + +### 4.3 对位综合评分 + +**主队对位优势**: +``` +主队对位优势 = 主队球星对位优势 - 客队球星对位优势 + + 主队阵容克制优势 - 客队阵容克制优势 +``` + +--- + +## 五、实时状态分析算法 + +### 5.1 比赛进行中的实时分析 + +#### 5.1.1 当前分差分析 + +**分差优势评分**: +``` +分差优势评分 = (当前分差 / 最大可能分差) × 时间调整系数 +``` + +**时间调整系数**: +- 比赛早期(第1-2节):系数 = 0.3(分差影响较小) +- 比赛中期(第3节):系数 = 0.6(分差影响中等) +- 比赛后期(第4节):系数 = 1.0(分差影响最大) +- 最后5分钟:系数 = 1.2(分差影响极大) + +#### 5.1.2 势头动量分析 + +**势头动量评分**: +``` +势头动量 = (过去N回合净胜分) / (N × 平均单回合得分) +``` + +**评分说明**: +- 如果主队过去 10 回合净胜 +8 分,则主队势头强劲 +- 势头动量 > 0.3:势头强劲 +- 势头动量 < -0.3:势头疲软 + +#### 5.1.3 实时综合评分调整 + +**实时调整公式**: +``` +实时调整评分 = 基础综合评分 + + 分差优势评分 × 0.3 + + 势头动量评分 × 0.2 + - 时间衰减系数 × 0.1 +``` + +**时间衰减系数**: +- 比赛越接近结束,基础评分的影响越小,实时状态的影响越大 +- 时间衰减系数 = (剩余时间 / 总时间) × 0.5 + +### 5.2 比赛未开始时的预测分析 + +**预测综合评分**: +``` +预测综合评分 = 基础综合评分 + + 对位优势评分 × 0.2 + + 环境因素评分 × 0.1 +``` + +--- + +## 六、获胜概率计算 + +### 6.1 基础获胜概率 + +**基于综合评分的概率**: +``` +基础获胜概率 = 1 / (1 + exp(-k × (主队综合评分 - 客队综合评分))) +``` + +**参数说明**: +- k 为调整系数,通常取 5-10 +- 如果主队综合评分 > 客队综合评分,则主队获胜概率 > 0.5 +- 如果主队综合评分 = 客队综合评分,则主队获胜概率 = 0.5 + +### 6.2 对位调整概率 + +**对位调整**: +``` +调整后概率 = 基础获胜概率 + 对位优势调整值 +``` + +**对位优势调整值**: +- 如果主队对位优势 > 0.1,则调整值 = +0.05 +- 如果主队对位优势 < -0.1,则调整值 = -0.05 +- 调整值范围:[-0.1, +0.1] + +### 6.3 实时状态调整概率 + +**实时调整**(仅比赛进行中): +``` +最终概率 = 调整后概率 + + 分差调整值 × 0.3 + + 势头调整值 × 0.2 +``` + +**分差调整值**: +- 如果主队领先,且剩余时间充足,则调整值 = +0.05 +- 如果主队落后,且剩余时间不足,则调整值 = -0.05 + +**势头调整值**: +- 如果主队势头强劲,则调整值 = +0.03 +- 如果主队势头疲软,则调整值 = -0.03 + +### 6.4 概率归一化 + +**确保概率在 [0, 1] 范围内**: +``` +最终概率 = max(0, min(1, 最终概率)) +``` + +--- + +## 七、交易价值计算 + +### 7.1 预期收益计算 + +**预期收益公式**: +``` +预期收益 = (获胜概率 × 获胜收益) - ((1 - 获胜概率) × 失败损失) +``` + +**收益和损失**: +- 如果买入主队获胜(YES),价格为 P + - 获胜收益 = (1 - P) × 投入金额 + - 失败损失 = P × 投入金额 +- 如果买入主队失败(NO),价格为 P + - 获胜收益 = P × 投入金额 + - 失败损失 = (1 - P) × 投入金额 + +### 7.2 风险调整收益 + +**风险调整公式**: +``` +风险调整收益 = 预期收益 × (1 - 风险系数) +``` + +**风险系数计算**: +``` +风险系数 = 概率不确定性 × 0.5 + 价格波动风险 × 0.3 + 流动性风险 × 0.2 +``` + +**风险组成**: +- **概率不确定性**:如果获胜概率接近 0.5,则不确定性高 + - 不确定性 = 1 - |获胜概率 - 0.5| × 2 +- **价格波动风险**:基于市场价格的波动性 + - 如果价格波动大,则风险高 +- **流动性风险**:基于市场的流动性 + - 如果市场流动性低,则风险高 + +### 7.3 交易价值评分 + +**交易价值评分**: +``` +交易价值评分 = 风险调整收益 / 投入金额 +``` + +**评分说明**: +- 交易价值评分 > 0.1:高价值交易,强烈推荐 +- 交易价值评分 > 0.05:中等价值交易,推荐 +- 交易价值评分 > 0:低价值交易,可考虑 +- 交易价值评分 <= 0:无价值交易,不推荐 + +--- + +## 八、交易信号生成算法 + +### 8.1 信号生成条件 + +**买入信号生成条件**: +1. **获胜概率条件**: + - 主队获胜概率 > 阈值(如 0.55)或 < 阈值(如 0.45) + - 如果主队获胜概率 > 0.55,买入主队获胜(YES) + - 如果主队获胜概率 < 0.45,买入主队失败(NO) + +2. **交易价值条件**: + - 交易价值评分 > 用户配置的最小交易价值(如 0.05) + +3. **价格条件**: + - 当前市场价格在合理范围内 + - 价格偏差在用户配置的容忍度内 + +4. **时间条件**: + - 比赛未结束 + - 剩余时间充足(如果用户配置了最小剩余时间) + +### 8.2 信号方向确定 + +**方向判断逻辑**: +``` +如果 主队获胜概率 > 0.55: + 方向 = YES(买入主队获胜) + 目标价格 = 市场价格(如果合理) + +如果 主队获胜概率 < 0.45: + 方向 = NO(买入主队失败) + 目标价格 = 1 - 市场价格(如果合理) + +如果 0.45 <= 主队获胜概率 <= 0.55: + 不生成信号(概率太接近,不确定性高) +``` + +### 8.3 信号价格计算 + +**价格计算策略**: + +**策略一:固定价格**(用户配置) +``` +信号价格 = 用户配置的固定价格 +``` + +**策略二:市场价格**(默认) +``` +信号价格 = 当前市场价格 +``` + +**策略三:动态价格**(基于概率) +``` +如果 方向 = YES: + 信号价格 = 主队获胜概率 × (1 + 价格偏移) + +如果 方向 = NO: + 信号价格 = (1 - 主队获胜概率) × (1 + 价格偏移) +``` + +**价格偏移**: +- 价格偏移 = 用户配置的价格偏移百分比(如 ±5%) +- 用于调整价格,提高交易成功率 + +### 8.4 信号数量计算 + +**数量计算策略**: + +**策略一:固定金额**(用户配置) +``` +信号数量 = 用户配置的固定金额 / 信号价格 +``` + +**策略二:按比例**(用户配置) +``` +信号数量 = 账户余额 × 用户配置的比例 / 信号价格 +``` + +**策略三:动态计算**(基于交易价值) +``` +基础数量 = 用户配置的基础金额 / 信号价格 +调整系数 = min(交易价值评分 / 0.1, 2.0) // 最多放大2倍 +信号数量 = 基础数量 × 调整系数 +``` + +### 8.5 信号原因生成 + +**触发原因说明**: +``` +触发原因 = "主队综合评分: {主队评分}, 客队综合评分: {客队评分}, " + + "获胜概率: {获胜概率}, 交易价值: {交易价值评分}, " + + "主要优势: {主要优势项}" +``` + +**主要优势项识别**: +- 如果基础实力评分差异最大,则主要优势 = "基础实力" +- 如果近期状态评分差异最大,则主要优势 = "近期状态" +- 如果对位优势明显,则主要优势 = "对位优势" +- 如果实时状态优势明显,则主要优势 = "实时状态" + +--- + +## 九、卖出信号生成算法 + +### 9.1 卖出条件 + +**卖出信号生成条件**: +1. **止盈条件**: + - 当前持仓的预期收益达到用户配置的止盈阈值(如 20%) + - 或市场价格达到用户配置的目标价格 + +2. **止损条件**: + - 当前持仓的预期亏损达到用户配置的止损阈值(如 -10%) + - 或市场价格跌破用户配置的止损价格 + +3. **概率反转条件**: + - 获胜概率发生反转(如从 0.6 降到 0.4) + - 反转幅度超过用户配置的阈值(如 0.15) + +4. **时间条件**: + - 比赛接近结束(剩余时间 < 用户配置的最小剩余时间) + - 或比赛已结束 + +### 9.2 卖出价格计算 + +**卖出价格**: +``` +卖出价格 = 当前市场价格 +``` + +**价格调整**(可选): +``` +如果 止盈卖出: + 卖出价格 = min(当前市场价格, 目标价格) + +如果 止损卖出: + 卖出价格 = max(当前市场价格, 止损价格) +``` + +### 9.3 卖出数量计算 + +**卖出数量**: +``` +如果 全部卖出: + 卖出数量 = 当前持仓数量 + +如果 部分卖出: + 卖出数量 = 当前持仓数量 × 用户配置的卖出比例 +``` + +--- + +## 十、风险控制算法 + +### 10.1 持仓限制检查 + +**最大持仓检查**: +``` +如果 信号数量 × 信号价格 > 用户配置的最大持仓: + 信号数量 = 用户配置的最大持仓 / 信号价格 + 记录警告:超过最大持仓限制,已调整数量 +``` + +**最小持仓检查**: +``` +如果 信号数量 × 信号价格 < 用户配置的最小持仓: + 不生成信号 + 记录原因:金额低于最小持仓限制 +``` + +### 10.2 每日限制检查 + +**每日亏损限制**: +``` +今日已亏损 = 今日所有交易的累计亏损 + +如果 今日已亏损 + 预期最大亏损 > 用户配置的每日亏损限制: + 不生成信号 + 记录原因:超过每日亏损限制 +``` + +**每日订单限制**: +``` +今日订单数 = 今日已生成的信号数量 + +如果 今日订单数 >= 用户配置的每日订单限制: + 不生成信号 + 记录原因:超过每日订单限制 +``` + +### 10.3 价格容忍度检查 + +**价格偏差检查**: +``` +价格偏差 = |信号价格 - 当前市场价格| / 当前市场价格 + +如果 价格偏差 > 用户配置的价格容忍度: + 不生成信号 + 记录原因:价格偏差超过容忍度 +``` + +### 10.4 概率置信度检查 + +**概率置信度**: +``` +如果 0.45 <= 获胜概率 <= 0.55: + 不生成信号 + 记录原因:获胜概率太接近,不确定性高 +``` + +**最小概率阈值**(可选): +``` +如果 获胜概率 < 用户配置的最小概率阈值: + 不生成信号 + 记录原因:获胜概率低于最小阈值 +``` + +--- + +## 十一、算法参数配置 + +### 11.1 用户可配置参数 + +**触发条件参数**: +- 最小获胜概率差异:主队和客队获胜概率的最小差异(如 0.1) +- 最小交易价值:交易价值评分的最小值(如 0.05) +- 最小剩余时间:生成信号时的最小剩余时间(如 5 分钟) + +**交易规则参数**: +- 买入金额策略:固定金额/按比例/动态计算 +- 买入金额:固定金额或比例值 +- 价格策略:固定价格/市场价格/动态价格 +- 价格偏移:价格偏移百分比(如 ±5%) + +**风险控制参数**: +- 最大持仓:单次最大买入金额 +- 最小持仓:单次最小买入金额 +- 每日亏损限制:每日最大亏损金额 +- 每日订单限制:每日最大订单数量 +- 价格容忍度:允许的价格偏差百分比 + +**卖出规则参数**: +- 止盈阈值:预期收益达到多少时卖出(如 20%) +- 止损阈值:预期亏损达到多少时卖出(如 -10%) +- 概率反转阈值:概率反转多少时卖出(如 0.15) +- 卖出比例:部分卖出时的比例(如 50%) + +### 11.2 系统默认参数 + +**评分权重**(可根据历史数据优化): +- 基础实力权重:0.3 +- 近期状态权重:0.25 +- 阵容完整度权重:0.2 +- 球星状态权重:0.15 +- 环境因素权重:0.1 + +**概率计算参数**: +- k 值(逻辑回归系数):7.5 +- 对位调整范围:[-0.1, +0.1] +- 实时调整权重:分差 0.3,势头 0.2 + +**风险系数权重**: +- 概率不确定性权重:0.5 +- 价格波动风险权重:0.3 +- 流动性风险权重:0.2 + +--- + +## 十二、算法优化建议 + +### 12.1 参数优化 + +**历史回测优化**: +- 使用历史数据回测不同参数组合 +- 找到最优的参数配置 +- 定期重新优化参数 + +**A/B 测试**: +- 同时运行多组参数配置 +- 对比不同参数的效果 +- 选择最优参数组合 + +### 12.2 模型优化 + +**机器学习集成**: +- 使用机器学习模型预测获胜概率 +- 使用梯度提升决策树(XGBoost/LightGBM) +- 定期重新训练模型 + +**特征工程优化**: +- 添加更多特征(如历史对战数据、伤病信息等) +- 使用特征选择技术筛选重要特征 +- 使用特征交互提高预测准确性 + +### 12.3 实时优化 + +**动态权重调整**: +- 根据比赛阶段动态调整权重 +- 比赛早期:基础实力权重高 +- 比赛后期:实时状态权重高 + +**自适应阈值**: +- 根据市场情况动态调整阈值 +- 如果市场波动大,提高概率阈值 +- 如果市场稳定,降低概率阈值 + +--- + +## 十三、算法输出 + +### 13.1 信号输出格式 + +**买入信号**: +``` +{ + "signalType": "BUY", + "strategyId": "策略ID", + "gameId": "比赛ID", + "marketId": "市场ID", + "direction": "YES" | "NO", + "price": 0.65, + "quantity": 10.0, + "totalAmount": 6.5, + "reason": "主队综合评分: 0.72, 客队综合评分: 0.58, 获胜概率: 0.62, 交易价值: 0.08, 主要优势: 基础实力", + "winProbability": 0.62, + "tradeValue": 0.08, + "timestamp": 1234567890 +} +``` + +**卖出信号**: +``` +{ + "signalType": "SELL", + "strategyId": "策略ID", + "gameId": "比赛ID", + "marketId": "市场ID", + "direction": "YES" | "NO", + "price": 0.75, + "quantity": 10.0, + "totalAmount": 7.5, + "reason": "止盈卖出,预期收益: 15%", + "profit": 1.0, + "profitRate": 0.15, + "timestamp": 1234567890 +} +``` + +### 13.2 算法执行日志 + +**执行日志格式**: +``` +{ + "timestamp": 1234567890, + "strategyId": "策略ID", + "gameId": "比赛ID", + "step": "数据获取" | "特征提取" | "评分计算" | "信号生成", + "status": "SUCCESS" | "FAILED" | "SKIPPED", + "message": "执行信息", + "data": {} // 相关数据 +} +``` + +--- + +## 十四、算法验证 + +### 14.1 回测验证 + +**回测流程**: +1. 使用历史比赛数据 +2. 模拟算法执行过程 +3. 计算信号生成情况 +4. 计算交易结果和盈亏 +5. 评估算法效果 + +**评估指标**: +- 信号生成数量 +- 信号准确率(实际获胜 vs 预测获胜) +- 总盈亏金额 +- 盈亏比率 +- 最大回撤 + +### 14.2 实时验证 + +**实时监控**: +- 监控算法执行情况 +- 记录信号生成和交易结果 +- 对比预测和实际结果 +- 评估算法实时表现 + +**持续优化**: +- 根据实时表现调整参数 +- 优化算法逻辑 +- 提高预测准确性 + +--- + +**文档结束** + diff --git a/docs/zh/nba-quantitative-trading-product-requirements.md b/docs/zh/nba-quantitative-trading-product-requirements.md new file mode 100644 index 0000000..20ec6d5 --- /dev/null +++ b/docs/zh/nba-quantitative-trading-product-requirements.md @@ -0,0 +1,732 @@ +# NBA 量化交易系统产品需求文档 + +## 文档信息 + +- **文档版本**: v1.0 +- **创建日期**: 2024-12 +- **文档类型**: 产品需求文档 (PRD) +- **目标用户**: 产品经理、开发团队、测试团队 + +--- + +## 一、产品概述 + +### 1.1 产品定位 + +NBA 量化交易系统是一个基于 NBA 比赛数据的智能量化交易平台,帮助用户通过配置量化策略参数,自动分析 NBA 比赛数据,生成买入/卖出交易信号,实现自动化交易决策。 + +### 1.2 产品目标 + +**核心目标**: +- 提供简单易用的策略配置界面,降低量化交易门槛 +- 实时获取和分析 NBA 比赛数据,提供及时的交易信号 +- 支持多种量化策略,满足不同用户需求 +- 提供完善的风险控制机制,保障交易安全 + +**用户价值**: +- **自动化决策**:无需人工盯盘,系统自动分析并生成交易信号 +- **数据驱动**:基于专业的 NBA 数据分析,提高决策准确性 +- **风险可控**:完善的风险控制参数,降低交易风险 +- **实时响应**:实时数据更新和信号推送,不错过交易机会 + +### 1.3 目标用户 + +**主要用户群体**: +1. **量化交易爱好者**:对 NBA 和量化交易都有兴趣的用户 +2. **NBA 数据分析师**:希望将数据分析转化为交易信号的用户 +3. **自动化交易用户**:希望通过自动化系统进行交易的用户 + +**用户特征**: +- 对 NBA 比赛有一定了解 +- 对量化交易有基本认知 +- 希望通过数据驱动的方式进行交易决策 +- 需要自动化工具提高交易效率 + +--- + +## 二、产品功能需求 + +### 2.1 策略配置管理 + +#### 2.1.1 策略创建 + +**功能描述**: +用户可以在前端创建新的量化策略,配置策略的各项参数。 + +**功能点**: +- **策略基本信息**: + - 策略名称:用户自定义策略名称(必填,1-50 字符) + - 策略描述:策略的简要说明(可选,最多 200 字符) + - 关联账户:选择用于交易的账户(必填) + - 启用状态:创建时默认启用,可后续修改 + +- **触发条件配置**: + - 比赛筛选: + - 按球队筛选:选择关注的球队(可选,不选择则分析所有比赛) + - 按日期筛选:选择关注的日期范围 + - 按重要性筛选:选择比赛重要性(常规赛/季后赛/关键战) + - 数据指标选择: + - 基础指标:分差、剩余时间、当前节次、比赛状态 + - 统计指标:球队净效率值、球员 PER、真实命中率等 + - 高级指标:势头动量、球星爆发因子、阵容对位系数等 + - 说明:系统会综合分析两支队伍的各项指标,计算综合评分和获胜概率 + - 触发阈值设置: + - 最小获胜概率差异:主队和客队获胜概率的最小差异(如 0.1 表示至少 10% 的差异) + - 最小交易价值:交易价值评分的最小值(如 0.05) + - 最小剩余时间:生成信号时的最小剩余时间(如 5 分钟,可选) + - 说明:只有当系统计算出某支队伍获胜概率明显高于对手,且交易价值达到阈值时,才会生成买入信号 + +- **交易规则配置**: + - 买入规则: + - 买入条件:系统自动分析两支队伍,当某支队伍获胜概率明显高于对手且交易价值达到阈值时触发买入 + - 买入金额:固定金额或按比例计算或动态计算(基于交易价值) + - 买入时机:立即买入或延迟买入 + - 买入方向:由系统自动判断(YES 或 NO,基于获胜概率高的队伍) + - 说明:系统会综合分析两支队伍,选择获胜概率更高、交易价值更大的方向进行买入 + - 卖出规则: + - 卖出条件:满足止盈、止损或概率反转条件时触发卖出 + - 卖出金额:全部卖出或部分卖出 + - 卖出时机:立即卖出或延迟卖出 + - 价格策略: + - 固定价格:使用固定价格 + - 市场价格:使用当前市场价格 + - 动态价格:根据获胜概率和市场情况动态计算价格 + - 价格偏移:允许的价格偏差范围(用于调整价格,提高交易成功率) + +- **风险控制配置**: + - 持仓限制: + - 最大持仓:单次最大买入金额(必填,>= 1 USDC) + - 最小持仓:单次最小买入金额(必填,>= 1 USDC) + - 每日限制: + - 每日亏损限制:每日最大亏损金额(可选) + - 每日订单限制:每日最大订单数量(可选) + - 价格容忍度: + - 价格偏差范围:允许的价格偏差百分比(可选,0-100%) + +- **高级配置**: + - 数据更新频率:NBA 数据更新频率(30 秒/1 分钟/5 分钟) + - 分析频率:量化分析执行频率(30 秒/1 分钟/5 分钟) + - 推送设置: + - 是否推送失败订单:默认关闭 + - 推送频率:实时推送/批量推送 + +**交互流程**: +1. 用户点击"创建策略"按钮 +2. 进入策略配置页面 +3. 填写策略基本信息 +4. 配置触发条件(可添加多个条件) +5. 配置交易规则 +6. 配置风险控制参数 +7. 配置高级选项 +8. 点击"保存"按钮,系统验证配置 +9. 保存成功,返回策略列表 + +**验证规则**: +- 策略名称不能为空,不能重复 +- 必须选择关联账户 +- 必须配置至少一个触发条件 +- 必须配置买入或卖出规则 +- 最大持仓必须 >= 最小持仓 +- 价格容忍度必须在 0-100% 范围内 + +#### 2.1.2 策略编辑 + +**功能描述**: +用户可以编辑已有策略的配置参数。 + +**功能点**: +- 支持修改策略的所有配置参数 +- 支持启用/禁用策略 +- 修改后立即生效(如果策略正在运行,会重新加载配置) + +**交互流程**: +1. 用户在策略列表中点击"编辑"按钮 +2. 进入策略编辑页面(与创建页面类似) +3. 修改配置参数 +4. 点击"保存"按钮 +5. 系统验证并保存配置 +6. 返回策略列表 + +#### 2.1.3 策略删除 + +**功能描述**: +用户可以删除不需要的策略。 + +**功能点**: +- 删除前需要确认(防止误删) +- 删除策略时,会停止该策略的所有量化任务 +- 删除后,该策略的历史信号记录会保留(用于统计分析) + +**交互流程**: +1. 用户在策略列表中点击"删除"按钮 +2. 弹出确认对话框 +3. 用户确认删除 +4. 系统删除策略并停止相关任务 +5. 返回策略列表 + +#### 2.1.4 策略列表 + +**功能描述**: +展示所有策略配置,支持搜索、筛选、排序。 + +**功能点**: +- **列表展示**: + - 策略名称 + - 关联账户 + - 启用状态(启用/禁用) + - 创建时间 + - 最后更新时间 + - 操作按钮(编辑/删除/启用/禁用) + +- **搜索功能**: + - 按策略名称搜索 + - 按账户名称搜索 + +- **筛选功能**: + - 按启用状态筛选(全部/启用/禁用) + - 按账户筛选 + +- **排序功能**: + - 按创建时间排序(最新/最旧) + - 按更新时间排序(最新/最旧) + - 按策略名称排序(A-Z/Z-A) + +- **分页功能**: + - 每页显示 20 条记录 + - 支持翻页 + +**交互流程**: +1. 用户进入策略列表页面 +2. 系统加载策略列表 +3. 用户可以搜索、筛选、排序 +4. 用户可以点击操作按钮进行编辑、删除、启用/禁用 + +### 2.2 交易信号展示 + +#### 2.2.1 实时信号列表 + +**功能描述**: +实时展示系统生成的交易信号。 + +**功能点**: +- **信号展示**: + - 信号类型(买入/卖出) + - 策略名称 + - 市场信息(市场 ID、市场标题) + - 方向(YES/NO) + - 价格 + - 数量 + - 触发原因 + - 生成时间 + - 信号状态(已生成/执行中/执行成功/执行失败) + +- **实时更新**: + - 通过 WebSocket 实时接收新信号 + - 新信号自动添加到列表顶部 + - 信号状态更新时实时刷新 + +- **筛选功能**: + - 按信号类型筛选(全部/买入/卖出) + - 按策略筛选 + - 按状态筛选(全部/已生成/执行中/成功/失败) + - 按时间范围筛选 + +- **排序功能**: + - 按生成时间排序(最新/最旧) + - 按价格排序(高到低/低到高) + +- **分页功能**: + - 每页显示 50 条记录 + - 支持翻页 + +**交互流程**: +1. 用户进入交易信号页面 +2. 系统建立 WebSocket 连接 +3. 系统订阅交易信号推送 +4. 实时接收并展示信号 +5. 用户可以筛选、排序、查看详情 + +#### 2.2.2 信号详情 + +**功能描述**: +展示信号的详细信息。 + +**功能点**: +- **基本信息**: + - 信号 ID + - 信号类型(买入/卖出) + - 策略名称 + - 生成时间 + +- **市场信息**: + - 市场 ID + - 市场标题 + - 市场描述 + - 市场分类 + +- **交易信息**: + - 方向(YES/NO) + - 价格 + - 数量 + - 总金额 + +- **触发信息**: + - 触发原因(详细说明) + - 触发条件(展示满足的条件) + - 触发时的比赛数据(分差、剩余时间等) + +- **执行信息**: + - 执行状态 + - 执行结果 + - 执行时间 + - 错误信息(如果执行失败) + +**交互流程**: +1. 用户在信号列表中点击某个信号 +2. 弹出信号详情对话框 +3. 展示信号的详细信息 +4. 用户可以关闭对话框 + +### 2.3 结果统计 + +#### 2.3.1 策略统计 + +**功能描述**: +展示每个策略的统计信息。 + +**功能点**: +- **统计指标**: + - 信号总数:该策略生成的信号总数 + - 买入信号数:买入信号数量 + - 卖出信号数:卖出信号数量 + - 成功率:执行成功的信号占比 + - 总盈亏:该策略的总盈亏金额 + - 平均盈亏:平均每个信号的盈亏金额 + +- **时间维度**: + - 今日统计 + - 本周统计 + - 本月统计 + - 全部统计 + +- **可视化展示**: + - 使用图表展示统计趋势 + - 使用饼图展示信号类型分布 + - 使用柱状图展示每日信号数量 + +**交互流程**: +1. 用户进入策略统计页面 +2. 系统加载策略列表和统计信息 +3. 用户选择某个策略查看详细统计 +4. 用户可以切换时间维度查看不同时期的统计 + +#### 2.3.2 总体统计 + +**功能描述**: +展示所有策略的总体统计信息。 + +**功能点**: +- **总体指标**: + - 总策略数:启用的策略数量 + - 总信号数:所有策略生成的信号总数 + - 总成功率:所有信号的平均成功率 + - 总盈亏:所有策略的总盈亏金额 + - 平均盈亏:平均每个信号的盈亏金额 + +- **趋势分析**: + - 信号数量趋势(按日/周/月) + - 成功率趋势(按日/周/月) + - 盈亏趋势(按日/周/月) + +- **排行榜**: + - 信号数量排行榜(按策略) + - 成功率排行榜(按策略) + - 盈亏排行榜(按策略) + +**交互流程**: +1. 用户进入总体统计页面 +2. 系统加载总体统计信息 +3. 用户可以查看趋势图表和排行榜 +4. 用户可以切换时间维度查看不同时期的统计 + +### 2.4 NBA 数据展示(可选功能) + +#### 2.4.1 比赛数据展示 + +**功能描述**: +展示 NBA 比赛的实时数据。 + +**功能点**: +- **比赛列表**: + - 展示今日/本周/本月的比赛 + - 显示比赛状态(未开始/进行中/已结束) + - 显示比分和分差 + +- **比赛详情**: + - 比赛基本信息(日期、主客场、比分等) + - 实时比分和分差 + - 当前节次和剩余时间 + - Play-by-Play 数据 + +**交互流程**: +1. 用户进入 NBA 数据页面 +2. 系统加载比赛列表 +3. 用户可以查看比赛详情 +4. 实时更新比赛数据(如果比赛正在进行) + +--- + +## 三、用户场景 + +### 3.1 场景一:创建量化策略 + +**用户**:量化交易爱好者 + +**场景描述**: +用户希望创建一个量化策略,系统自动分析比赛的两支队伍,根据综合评分和获胜概率,选择可盈利的队伍进行买入。 + +**操作流程**: +1. 用户登录系统,进入策略配置页面 +2. 点击"创建策略"按钮 +3. 填写策略名称:"综合实力分析策略" +4. 选择关联账户 +5. 配置触发条件: + - 比赛筛选:选择关注的比赛(可按球队、日期筛选) + - 数据指标选择:选择用于分析的指标 + - 基础指标:分差、剩余时间、当前节次 + - 统计指标:球队净效率值、球员PER、真实命中率 + - 高级指标:势头动量、球星爆发因子、阵容对位系数 + - 触发阈值设置: + - 最小获胜概率差异:0.1(主队和客队获胜概率差异至少 10%) + - 最小交易价值:0.05(交易价值评分至少 0.05) +6. 配置买入规则: + - 买入条件:当系统计算出某支队伍获胜概率明显高于对手,且交易价值评分达到阈值时触发买入 + - 买入金额:固定金额 10 USDC + - 买入方向:由系统自动判断(YES 或 NO,基于获胜概率) + - 价格策略:动态价格(基于获胜概率和市场情况) +7. 配置风险控制: + - 最大持仓:50 USDC + - 最小持仓:5 USDC + - 每日亏损限制:100 USDC +8. 点击"保存"按钮 +9. 系统验证配置并保存 +10. 策略创建成功,自动启用 + +**预期结果**: +- 策略创建成功并启用 +- 系统开始监控 NBA 比赛数据 +- 当满足条件时,自动生成买入信号 + +### 3.2 场景二:查看交易信号 + +**用户**:量化交易爱好者 + +**场景描述**: +用户希望实时查看系统生成的交易信号,了解策略的执行情况。 + +**操作流程**: +1. 用户登录系统,进入交易信号页面 +2. 系统自动建立 WebSocket 连接 +3. 系统订阅交易信号推送 +4. 实时接收并展示新信号 +5. 用户可以查看信号详情: + - 点击某个信号,查看详细信息 + - 查看触发原因和触发时的比赛数据 + - 查看执行状态和结果 +6. 用户可以筛选信号: + - 按策略筛选:只查看特定策略的信号 + - 按类型筛选:只查看买入或卖出信号 + - 按状态筛选:只查看成功或失败的信号 + +**预期结果**: +- 实时接收并展示交易信号 +- 可以查看信号的详细信息 +- 可以按需筛选和排序信号 + +### 3.3 场景三:查看策略统计 + +**用户**:量化交易爱好者 + +**场景描述**: +用户希望查看策略的执行效果,了解策略的盈亏情况。 + +**操作流程**: +1. 用户登录系统,进入策略统计页面 +2. 系统加载所有策略的统计信息 +3. 用户选择某个策略查看详细统计: + - 查看信号总数、成功率、总盈亏等指标 + - 查看信号数量趋势图 + - 查看盈亏趋势图 +4. 用户可以切换时间维度: + - 查看今日统计 + - 查看本周统计 + - 查看本月统计 + - 查看全部统计 +5. 用户可以查看总体统计: + - 查看所有策略的总体指标 + - 查看趋势分析图表 + - 查看排行榜 + +**预期结果**: +- 清晰展示策略的执行效果 +- 提供多维度的时间统计 +- 帮助用户优化策略配置 + +--- + +## 四、界面设计说明 + +### 4.1 策略配置页面 + +#### 4.1.1 页面布局 + +**整体布局**: +- 顶部:页面标题"创建策略"或"编辑策略" +- 左侧:配置导航(基本信息、触发条件、交易规则、风险控制、高级配置) +- 中间:配置表单区域 +- 底部:操作按钮(保存、取消) + +**配置表单**: +- 使用分步骤表单,引导用户逐步配置 +- 每个步骤有清晰的说明和示例 +- 必填项用红色星号标记 +- 提供实时验证和错误提示 + +#### 4.1.2 关键组件 + +**触发条件配置组件**: +- 条件列表:展示已添加的触发条件 +- 添加条件按钮:点击后弹出条件配置对话框 +- 条件编辑:支持编辑和删除已有条件 +- 条件组合:支持设置条件之间的逻辑关系(AND/OR) + +**交易规则配置组件**: +- 买入规则配置区域 +- 卖出规则配置区域 +- 价格策略选择(固定价格/动态价格) +- 价格偏移设置 + +**风险控制配置组件**: +- 持仓限制输入框 +- 每日限制输入框 +- 价格容忍度滑块 + +### 4.2 交易信号页面 + +#### 4.2.1 页面布局 + +**整体布局**: +- 顶部:筛选和搜索栏 +- 中间:信号列表(表格形式) +- 底部:分页组件 + +**信号列表表格**: +- 列:信号类型、策略名称、市场信息、方向、价格、数量、触发原因、生成时间、状态、操作 +- 支持列排序 +- 支持行点击查看详情 + +#### 4.2.2 关键组件 + +**实时信号提示**: +- 新信号到达时,顶部显示提示消息 +- 信号列表自动滚动到顶部 +- 新信号高亮显示(3 秒后恢复正常) + +**信号状态标签**: +- 已生成:蓝色标签 +- 执行中:黄色标签 +- 执行成功:绿色标签 +- 执行失败:红色标签 + +**信号详情对话框**: +- 模态对话框形式 +- 展示信号的完整信息 +- 支持关闭和复制信息 + +### 4.3 统计页面 + +#### 4.3.1 页面布局 + +**整体布局**: +- 顶部:时间维度选择(今日/本周/本月/全部) +- 左侧:策略列表(如果查看策略统计) +- 中间:统计图表和数据表格 +- 右侧:关键指标卡片 + +**统计图表**: +- 使用 ECharts 或类似图表库 +- 支持交互(缩放、筛选等) +- 支持导出图片 + +#### 4.3.2 关键组件 + +**指标卡片**: +- 大数字显示关键指标 +- 支持对比(与上期对比) +- 支持趋势箭头(上升/下降) + +**趋势图表**: +- 折线图:展示趋势变化 +- 柱状图:展示数量对比 +- 饼图:展示分布情况 + +--- + +## 五、非功能性需求 + +### 5.1 性能需求 + +**响应时间**: +- 页面加载时间:< 2 秒 +- API 响应时间:< 1 秒 +- WebSocket 推送延迟:< 100 毫秒 + +**并发性能**: +- 支持至少 100 个并发用户 +- 支持至少 1000 个策略同时运行 +- 支持至少 10000 个信号/天的处理能力 + +### 5.2 可用性需求 + +**系统可用性**: +- 系统可用性:>= 99.5% +- 故障恢复时间:< 5 分钟 + +**数据实时性**: +- NBA 数据更新延迟:< 1 分钟 +- 交易信号推送延迟:< 5 秒 + +### 5.3 安全性需求 + +**数据安全**: +- 用户配置数据加密存储 +- API 接口需要身份认证 +- WebSocket 连接需要身份认证 + +**风险控制**: +- 所有交易信号需要经过风险控制检查 +- 异常情况自动告警 +- 支持紧急停止策略 + +### 5.4 兼容性需求 + +**浏览器兼容**: +- Chrome(最新 2 个版本) +- Firefox(最新 2 个版本) +- Safari(最新 2 个版本) +- Edge(最新 2 个版本) + +**设备兼容**: +- 桌面端:1920x1080 及以上分辨率 +- 平板端:768x1024 及以上分辨率 +- 移动端:375x667 及以上分辨率(响应式设计) + +--- + +## 六、产品特性 + +### 6.1 核心特性 + +1. **简单易用**: + - 直观的配置界面,降低使用门槛 + - 清晰的步骤引导,帮助用户快速上手 + - 丰富的帮助文档和示例 + +2. **实时响应**: + - 实时获取 NBA 比赛数据 + - 实时执行量化分析 + - 实时推送交易信号 + +3. **数据驱动**: + - 基于专业的 NBA 数据分析 + - 支持多种数据指标和特征 + - 提供数据可视化展示 + +4. **风险可控**: + - 完善的风险控制参数 + - 自动风险检查机制 + - 异常情况告警 + +### 6.2 扩展特性 + +1. **策略模板**: + - 提供预定义的策略模板 + - 用户可以基于模板快速创建策略 + - 支持模板的导入和导出 + +2. **回测功能**: + - 支持历史数据回测 + - 评估策略的历史表现 + - 优化策略参数 + +3. **多账户支持**: + - 支持多个交易账户 + - 每个账户可以配置不同的策略 + - 统一管理和监控 + +--- + +## 七、产品路线图 + +### 7.1 第一阶段:MVP 版本(2-3 个月) + +**核心功能**: +- 策略配置管理(创建、编辑、删除、列表) +- 基础量化分析(简单策略逻辑) +- 交易信号生成和推送 +- 基础统计展示 + +**目标**: +- 验证产品可行性 +- 收集用户反馈 +- 优化核心功能 + +### 7.2 第二阶段:功能完善(2-3 个月) + +**新增功能**: +- 完善量化分析逻辑(支持复杂策略) +- 完善特征提取和数据处理 +- 完善统计和可视化 +- 策略模板功能 + +**目标**: +- 提升产品功能完整性 +- 提升用户体验 +- 扩大用户群体 + +### 7.3 第三阶段:高级功能(3-4 个月) + +**新增功能**: +- 回测功能 +- 机器学习集成 +- 多账户支持 +- 高级统计分析 + +**目标**: +- 提升产品竞争力 +- 满足高级用户需求 +- 建立产品壁垒 + +--- + +## 八、附录 + +### 8.1 术语表 + +- **量化策略**:基于数据分析和算法模型的交易策略 +- **交易信号**:系统生成的买入或卖出建议 +- **触发条件**:满足哪些条件时生成交易信号 +- **风险控制**:限制交易风险的各种参数和机制 +- **WebSocket**:一种实时通信协议,用于推送数据 + +### 8.2 参考文档 + +- NBA 比赛分析框架文档(NBA.md) +- 技术方案文档(nba-api-integration-technical-solution.md) +- API 接口文档(待补充) +- 用户手册(待补充) + +--- + +**文档结束** + diff --git a/docs/zh/nba-strategy-configurable-parameters.md b/docs/zh/nba-strategy-configurable-parameters.md new file mode 100644 index 0000000..2d5aa02 --- /dev/null +++ b/docs/zh/nba-strategy-configurable-parameters.md @@ -0,0 +1,567 @@ +# NBA 量化策略前端可配置参数清单 + +## 一、参数分类 + +前端可配置参数分为以下几类: +1. **策略基本信息**:策略名称、描述等 +2. **比赛筛选参数**:选择关注的比赛 +3. **触发条件参数**:控制何时生成交易信号 +4. **交易规则参数**:控制如何执行交易 +5. **风险控制参数**:限制交易风险 +6. **算法权重参数**(高级):调整算法内部权重 +7. **系统配置参数**:数据更新频率、推送设置等 + +--- + +## 二、详细参数列表 + +### 2.1 策略基本信息(必填) + +| 参数名称 | 类型 | 必填 | 默认值 | 说明 | UI 组件 | +|---------|------|------|--------|------|---------| +| **策略名称** | String | ✅ | - | 用户自定义策略名称,1-50 字符 | Input | +| **策略描述** | String | ❌ | - | 策略的简要说明,最多 200 字符 | TextArea | +| **关联账户** | Long | ✅ | - | 选择用于交易的账户 | Select | +| **启用状态** | Boolean | ❌ | true | 是否启用该策略 | Switch | + +--- + +### 2.2 比赛筛选参数(可选) + +| 参数名称 | 类型 | 必填 | 默认值 | 说明 | UI 组件 | +|---------|------|------|--------|------|---------| +| **关注球队** | List\ | ❌ | [] | 选择关注的球队(可选,不选择则分析所有比赛) | MultiSelect | +| **日期范围** | DateRange | ❌ | 今日 | 选择关注的日期范围 | DateRangePicker | +| **比赛重要性** | Enum | ❌ | 全部 | 选择比赛重要性(全部/常规赛/季后赛/关键战) | Select | +| **主客场筛选** | Enum | ❌ | 全部 | 筛选主队或客队(全部/仅主队/仅客队) | Select | + +--- + +### 2.3 触发条件参数(必填) + +#### 2.3.1 概率阈值参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **最小获胜概率差异** | Decimal | ✅ | 0.1 | 0.05 - 0.5 | 主队和客队获胜概率的最小差异(如 0.1 表示至少 10% 的差异) | InputNumber (0-1, 步长 0.01) | +| **最小获胜概率** | Decimal | ❌ | 0.55 | 0.5 - 1.0 | 生成买入信号时的最小获胜概率(可选,不设置则不限制) | InputNumber (0-1, 步长 0.01) | +| **最大获胜概率** | Decimal | ❌ | - | 0.0 - 0.5 | 生成买入信号时的最大获胜概率(可选,用于反向策略) | InputNumber (0-1, 步长 0.01) | + +#### 2.3.2 交易价值参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **最小交易价值** | Decimal | ✅ | 0.05 | 0.0 - 1.0 | 交易价值评分的最小值,只有达到此值才会生成信号 | InputNumber (0-1, 步长 0.01) | + +#### 2.3.3 时间条件参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **最小剩余时间** | Integer | ❌ | 5 | 0 - 48 | 生成信号时的最小剩余时间(分钟),0 表示不限制 | InputNumber (0-48) | +| **最大剩余时间** | Integer | ❌ | - | 0 - 48 | 生成信号时的最大剩余时间(分钟),可选 | InputNumber (0-48) | +| **比赛阶段限制** | Enum | ❌ | 全部 | 全部/仅比赛前/仅比赛中/仅比赛后 | 限制在哪个阶段生成信号 | Select | + +#### 2.3.4 分差条件参数(可选) + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **最小分差** | Integer | ❌ | - | -50 - 50 | 生成信号时的最小分差(主队得分 - 客队得分),可选 | InputNumber | +| **最大分差** | Integer | ❌ | - | -50 - 50 | 生成信号时的最大分差,可选 | InputNumber | +| **分差范围** | Enum | ❌ | 全部 | 全部/小分差(<5分)/中分差(5-15分)/大分差(>15分) | 限制分差范围 | Select | + +--- + +### 2.4 交易规则参数 + +#### 2.4.1 买入规则参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **买入金额策略** | Enum | ✅ | FIXED | FIXED/RATIO/DYNAMIC | 买入金额计算方式 | Select | +| **固定买入金额** | Decimal | 条件必填 | 10 | > 0 | 固定金额策略时的买入金额(USDC) | InputNumber | +| **买入比例** | Decimal | 条件必填 | 0.1 | 0.01 - 1.0 | 按比例策略时的买入比例(账户余额的百分比) | InputNumber (0-1, 步长 0.01) | +| **基础买入金额** | Decimal | 条件必填 | 10 | > 0 | 动态计算策略时的基础金额(USDC) | InputNumber | +| **买入时机** | Enum | ✅ | IMMEDIATE | IMMEDIATE/DELAYED | 立即买入或延迟买入 | Select | +| **延迟买入时间** | Integer | 条件必填 | 0 | 0 - 300 | 延迟买入的秒数(仅在延迟买入时生效) | InputNumber (0-300) | +| **买入方向** | Enum | ✅ | AUTO | AUTO/YES/NO | 买入方向:AUTO=系统自动判断,YES/NO=固定方向 | Select | +| **说明** | - | - | - | - | 系统会自动分析两支队伍,选择获胜概率更高、交易价值更大的方向 | Info | + +#### 2.4.2 卖出规则参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **启用卖出** | Boolean | ✅ | true | - | 是否启用卖出功能 | Switch | +| **止盈阈值** | Decimal | ❌ | 0.2 | 0.0 - 1.0 | 预期收益达到多少时卖出(如 0.2 表示 20%) | InputNumber (0-1, 步长 0.01) | +| **止损阈值** | Decimal | ❌ | -0.1 | -1.0 - 0.0 | 预期亏损达到多少时卖出(如 -0.1 表示 -10%) | InputNumber (-1-0, 步长 0.01) | +| **概率反转阈值** | Decimal | ❌ | 0.15 | 0.0 - 1.0 | 获胜概率反转多少时卖出(如 0.15 表示 15%) | InputNumber (0-1, 步长 0.01) | +| **卖出比例** | Decimal | ✅ | 1.0 | 0.1 - 1.0 | 卖出时的比例(1.0 表示全部卖出) | InputNumber (0-1, 步长 0.1) | +| **卖出时机** | Enum | ✅ | IMMEDIATE | IMMEDIATE/DELAYED | 立即卖出或延迟卖出 | Select | +| **延迟卖出时间** | Integer | 条件必填 | 0 | 0 - 300 | 延迟卖出的秒数(仅在延迟卖出时生效) | InputNumber (0-300) | + +#### 2.4.3 价格策略参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **价格策略** | Enum | ✅ | MARKET | FIXED/MARKET/DYNAMIC | 价格计算方式 | Select | +| **固定价格** | Decimal | 条件必填 | - | 0.01 - 0.99 | 固定价格策略时的价格(仅在固定价格时生效) | InputNumber (0-1, 步长 0.01) | +| **价格偏移** | Decimal | ❌ | 0.0 | -0.1 - 0.1 | 价格偏移百分比(用于调整价格,提高交易成功率,如 0.05 表示 +5%) | InputNumber (-0.1-0.1, 步长 0.01) | + +--- + +### 2.5 风险控制参数 + +#### 2.5.1 持仓限制参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **最大持仓** | Decimal | ✅ | 50 | >= 1 | 单次最大买入金额(USDC) | InputNumber (>= 1) | +| **最小持仓** | Decimal | ✅ | 5 | >= 1 | 单次最小买入金额(USDC) | InputNumber (>= 1) | +| **单场比赛最大持仓** | Decimal | ❌ | - | >= 1 | 单场比赛的最大持仓金额(USDC),可选 | InputNumber (>= 1) | + +#### 2.5.2 每日限制参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **每日亏损限制** | Decimal | ❌ | 100 | > 0 | 每日最大亏损金额(USDC),可选 | InputNumber (> 0) | +| **每日订单限制** | Integer | ❌ | 20 | > 0 | 每日最大订单数量,可选 | InputNumber (> 0) | +| **每日盈利目标** | Decimal | ❌ | - | > 0 | 每日盈利目标(USDC),达到后停止交易,可选 | InputNumber (> 0) | + +#### 2.5.3 价格容忍度参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **价格容忍度** | Decimal | ❌ | 0.05 | 0.0 - 1.0 | 允许的价格偏差百分比(如 0.05 表示 5%) | InputNumber (0-1, 步长 0.01) | + +#### 2.5.4 概率置信度参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **最小概率阈值** | Decimal | ❌ | - | 0.5 - 1.0 | 最小获胜概率阈值(可选,不设置则不限制) | InputNumber (0.5-1.0, 步长 0.01) | +| **最大概率阈值** | Decimal | ❌ | - | 0.0 - 0.5 | 最大获胜概率阈值(可选,用于反向策略) | InputNumber (0-0.5, 步长 0.01) | + +--- + +### 2.6 算法权重参数(高级,可选) + +#### 2.6.1 综合评分权重参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **基础实力权重** | Decimal | ❌ | 0.3 | 0.0 - 1.0 | 基础实力评分在综合评分中的权重 | InputNumber (0-1, 步长 0.05) | +| **近期状态权重** | Decimal | ❌ | 0.25 | 0.0 - 1.0 | 近期状态评分在综合评分中的权重 | InputNumber (0-1, 步长 0.05) | +| **阵容完整度权重** | Decimal | ❌ | 0.2 | 0.0 - 1.0 | 阵容完整度评分在综合评分中的权重 | InputNumber (0-1, 步长 0.05) | +| **球星状态权重** | Decimal | ❌ | 0.15 | 0.0 - 1.0 | 球星状态评分在综合评分中的权重 | InputNumber (0-1, 步长 0.05) | +| **环境因素权重** | Decimal | ❌ | 0.1 | 0.0 - 1.0 | 环境因素评分在综合评分中的权重 | InputNumber (0-1, 步长 0.05) | +| **说明** | - | - | - | - | 所有权重之和应该等于 1.0,系统会自动归一化 | Info | + +#### 2.6.2 对位分析权重参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **对位优势权重** | Decimal | ❌ | 0.2 | 0.0 - 1.0 | 对位优势在获胜概率调整中的权重 | InputNumber (0-1, 步长 0.05) | + +#### 2.6.3 实时状态权重参数(仅比赛进行中) + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **分差调整权重** | Decimal | ❌ | 0.3 | 0.0 - 1.0 | 分差调整在实时概率调整中的权重 | InputNumber (0-1, 步长 0.05) | +| **势头调整权重** | Decimal | ❌ | 0.2 | 0.0 - 1.0 | 势头调整在实时概率调整中的权重 | InputNumber (0-1, 步长 0.05) | + +--- + +### 2.7 系统配置参数 + +#### 2.7.1 数据更新参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **数据更新频率** | Integer | ✅ | 30 | 10/30/60/300 | NBA 数据更新频率(秒) | Select | +| **分析频率** | Integer | ✅ | 30 | 10/30/60/300 | 量化分析执行频率(秒) | Select | + +#### 2.7.2 推送设置参数 + +| 参数名称 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | UI 组件 | +|---------|------|------|--------|----------|------|---------| +| **推送失败订单** | Boolean | ❌ | false | - | 是否推送失败订单 | Switch | +| **推送频率** | Enum | ❌ | REALTIME | REALTIME/BATCH | 实时推送或批量推送 | Select | +| **批量推送间隔** | Integer | 条件必填 | 1 | 1 - 60 | 批量推送的时间间隔(秒,仅在批量推送时生效) | InputNumber (1-60) | + +--- + +## 三、参数分组和页面布局建议 + +### 3.1 参数分组 + +**基础配置组**(第一页): +- 策略基本信息 +- 比赛筛选参数 + +**触发条件组**(第二页): +- 概率阈值参数 +- 交易价值参数 +- 时间条件参数 +- 分差条件参数 + +**交易规则组**(第三页): +- 买入规则参数 +- 卖出规则参数 +- 价格策略参数 + +**风险控制组**(第四页): +- 持仓限制参数 +- 每日限制参数 +- 价格容忍度参数 +- 概率置信度参数 + +**高级配置组**(第五页,可折叠): +- 算法权重参数 +- 系统配置参数 + +### 3.2 UI 组件建议 + +**表单布局**: +- 使用分步骤表单(Stepper),引导用户逐步配置 +- 每个步骤有清晰的标题和说明 +- 必填项用红色星号标记 +- 提供实时验证和错误提示 + +**参数展示**: +- 使用折叠面板(Collapse)组织相关参数 +- 高级参数默认折叠,用户可展开查看 +- 提供参数说明和示例 +- 提供参数推荐值(基于历史数据) + +**交互优化**: +- 条件显示:某些参数只在特定条件下显示(如固定价格只在价格策略=固定时显示) +- 联动验证:相关参数之间进行联动验证(如最大持仓 >= 最小持仓) +- 实时预览:显示参数配置后的预期效果(如预期信号数量、预期交易频率等) + +--- + +## 四、参数验证规则 + +### 4.1 必填参数验证 + +- 策略名称:不能为空,1-50 字符,不能重复 +- 关联账户:必须选择 +- 最小获胜概率差异:必须 > 0 +- 最小交易价值:必须 >= 0 +- 最大持仓:必须 >= 1 +- 最小持仓:必须 >= 1 +- 数据更新频率:必须选择 +- 分析频率:必须选择 + +### 4.2 数值范围验证 + +- 概率相关参数:必须在 [0, 1] 范围内 +- 金额相关参数:必须 > 0 +- 时间相关参数:必须在合理范围内(如 0-48 分钟) +- 权重相关参数:必须在 [0, 1] 范围内,且总和应该接近 1.0 + +### 4.3 逻辑关系验证 + +- 最大持仓 >= 最小持仓 +- 最大剩余时间 >= 最小剩余时间(如果都设置了) +- 最大分差 >= 最小分差(如果都设置了) +- 最大概率阈值 <= 最小概率阈值(如果都设置了) +- 权重总和应该在 0.9 - 1.1 范围内(系统会自动归一化) + +### 4.4 条件依赖验证 + +- 如果买入金额策略 = FIXED,则固定买入金额必填 +- 如果买入金额策略 = RATIO,则买入比例必填 +- 如果买入金额策略 = DYNAMIC,则基础买入金额必填 +- 如果价格策略 = FIXED,则固定价格必填 +- 如果买入时机 = DELAYED,则延迟买入时间必填 +- 如果卖出时机 = DELAYED,则延迟卖出时间必填 +- 如果推送频率 = BATCH,则批量推送间隔必填 + +--- + +## 五、参数默认值建议 + +### 5.1 新手推荐配置 + +**保守策略**(适合新手): +- 最小获胜概率差异:0.15(更严格的条件) +- 最小交易价值:0.08(更高的价值要求) +- 最大持仓:20 USDC(较小的持仓) +- 最小持仓:5 USDC +- 每日亏损限制:50 USDC +- 价格容忍度:0.03(3%,较严格) + +**稳健策略**(推荐): +- 最小获胜概率差异:0.1(标准条件) +- 最小交易价值:0.05(标准价值) +- 最大持仓:50 USDC +- 最小持仓:5 USDC +- 每日亏损限制:100 USDC +- 价格容忍度:0.05(5%) + +**激进策略**(适合有经验的用户): +- 最小获胜概率差异:0.05(较宽松的条件) +- 最小交易价值:0.03(较低的价值要求) +- 最大持仓:100 USDC(较大的持仓) +- 最小持仓:10 USDC +- 每日亏损限制:200 USDC +- 价格容忍度:0.1(10%,较宽松) + +### 5.2 参数模板 + +系统可以提供预定义的参数模板: +- **保守模板**:使用保守策略的默认值 +- **稳健模板**:使用稳健策略的默认值 +- **激进模板**:使用激进策略的默认值 +- **自定义模板**:用户保存的自定义配置 + +### 5.3 基于历史数据的推荐值 + +#### 5.3.1 推荐值功能概述 + +系统可以根据历史数据分析和回测结果,为每个可配置参数提供智能推荐值,帮助用户快速配置策略,提高策略效果。 + +#### 5.3.2 推荐值计算方式 + +**方式一:基于历史回测的推荐值** + +**计算流程**: +1. 使用历史比赛数据(如过去 1-3 个赛季) +2. 对不同的参数组合进行回测 +3. 评估每个参数组合的效果(准确率、盈亏、风险等) +4. 选择最优的参数组合作为推荐值 + +**评估指标**: +- 信号准确率:预测获胜 vs 实际获胜的比例 +- 总盈亏:累计盈亏金额 +- 盈亏比率:盈利信号的平均盈亏 / 亏损信号的平均盈亏 +- 最大回撤:最大连续亏损 +- 夏普比率:风险调整后的收益 + +**推荐值选择**: +- 选择综合评分最高的参数组合 +- 综合评分 = 准确率 × 0.3 + 盈亏比率 × 0.3 + 夏普比率 × 0.2 + (1 - 最大回撤率) × 0.2 + +**方式二:基于用户策略的推荐值** + +**计算方式**: +- 分析用户已有策略的参数配置 +- 统计表现最好的策略的参数分布 +- 计算参数的平均值或中位数作为推荐值 + +**适用场景**: +- 新用户:使用所有用户的最佳策略参数 +- 老用户:使用该用户自己的最佳策略参数 + +**方式三:基于市场环境的推荐值** + +**动态调整**: +- 根据当前市场环境(波动性、流动性等)动态调整推荐值 +- 市场波动大时:提高概率阈值,降低持仓 +- 市场稳定时:降低概率阈值,提高持仓 + +**环境因素**: +- 市场波动性:基于近期价格的波动率 +- 市场流动性:基于订单深度和交易量 +- 比赛重要性:季后赛 vs 常规赛 + +#### 5.3.3 推荐值展示方式 + +**在参数输入框中的展示**: +- 在参数输入框旁边显示推荐值按钮(如"使用推荐值"图标) +- 点击后自动填充推荐值 +- 推荐值用不同颜色标识(如蓝色高亮) +- 显示推荐值的来源标签(如"基于历史回测"、"基于您的策略"、"基于市场环境") +- 显示推荐值的置信度(如"高置信度"、"中置信度"、"低置信度") + +**推荐值详细说明**: +- 点击推荐值按钮后,弹出推荐值说明卡片 +- 显示推荐值的计算依据 +- 显示使用该推荐值的预期效果(如"预期准确率 65%"、"预期盈亏比 1.5") +- 显示推荐值的适用场景(如"适合保守策略"、"适合激进策略") +- 显示推荐值的验证数据(如"基于 500 场历史比赛验证") + +**参数对比展示**: +- 在参数配置页面底部显示"推荐值对比"面板 +- 显示当前配置值 vs 推荐值的对比 +- 显示预期效果对比(如准确率、盈亏等) +- 提供"应用所有推荐值"按钮,一键应用所有推荐值 +- 提供"应用部分推荐值"选项,让用户选择应用哪些推荐值 + +#### 5.3.4 推荐值更新机制 + +**更新频率**: +- 每周更新一次:基于最新的历史数据 +- 每月深度更新:重新进行完整的回测分析 +- 实时更新:根据市场环境变化实时调整(仅市场环境相关参数) + +**触发更新**: +- 新赛季开始:使用新赛季的数据重新计算 +- 市场环境变化:检测到市场环境显著变化时更新 +- 用户请求:用户手动触发更新 + +**更新通知**: +- 在策略配置页面显示"推荐值已更新"提示 +- 显示更新内容(哪些参数有变化) +- 提供"查看更新"按钮,查看详细的更新说明 +- 提供"应用更新"按钮,一键应用更新的推荐值 + +#### 5.3.5 推荐值个性化 + +**基于用户偏好的推荐**: + +**偏好设置**: +- 风险偏好:保守/稳健/激进 +- 交易频率偏好:低频/中频/高频 +- 持仓偏好:小持仓/中持仓/大持仓 + +**个性化推荐**: +- 根据用户偏好筛选推荐值 +- 优先推荐符合用户偏好的参数组合 +- 提供多个推荐选项(保守推荐、稳健推荐、激进推荐) + +**基于策略类型的推荐**: + +**策略类型**: +- 保守策略:高概率阈值、小持仓、严格风险控制 +- 稳健策略:中等概率阈值、中等持仓、标准风险控制 +- 激进策略:低概率阈值、大持仓、宽松风险控制 + +**类型推荐**: +- 用户选择策略类型后,自动推荐该类型的参数值 +- 提供策略类型模板,一键应用 + +#### 5.3.6 推荐值验证和反馈 + +**推荐值验证**: + +**验证方式**: +- 使用历史数据验证推荐值的有效性 +- 计算推荐值的回测准确率 +- 评估推荐值的风险水平 + +**验证结果展示**: +- 显示推荐值的验证状态(如"已验证"、"待验证") +- 显示验证数据(如"基于 500 场历史比赛验证") +- 显示验证指标(如准确率、盈亏比等) +- 显示验证时间(如"2024-12-01 验证") + +**用户反馈机制**: + +**反馈收集**: +- 用户使用推荐值后,可以反馈效果 +- 收集用户对推荐值的满意度(1-5 星) +- 收集用户的实际使用效果数据(准确率、盈亏等) + +**反馈应用**: +- 根据用户反馈调整推荐算法 +- 优化推荐值的准确性 +- 提高推荐值的适用性 + +#### 5.3.7 推荐值功能实现 + +**前端实现**: + +**UI 组件**: +- 推荐值按钮:在参数输入框旁边显示推荐值图标按钮 +- 推荐值卡片:点击后显示推荐值的详细说明 +- 推荐值对比面板:显示当前值 vs 推荐值的对比 +- 推荐值应用按钮:一键应用所有推荐值 + +**交互流程**: +1. 用户进入策略配置页面 +2. 系统异步加载推荐值(不阻塞页面加载) +3. 在参数输入框旁边显示推荐值按钮(如果该参数有推荐值) +4. 用户点击推荐值按钮,查看推荐值详情 +5. 用户可以选择应用推荐值或手动调整 +6. 用户可以在底部查看所有推荐值的对比 + +**后端实现**: + +**推荐值计算服务**: +- 历史回测服务:执行历史数据回测 +- 推荐值计算服务:计算推荐值 +- 推荐值缓存服务:缓存推荐值,提高响应速度 + +**API 接口**: +- 获取推荐值接口:根据参数类型和用户偏好返回推荐值 +- 更新推荐值接口:触发推荐值更新 +- 推荐值验证接口:验证推荐值的有效性 +- 用户反馈接口:收集用户反馈 + +**数据存储**: + +**推荐值存储**: +- 存储推荐值计算结果 +- 存储推荐值的计算依据和验证结果 +- 存储用户反馈数据 + +**缓存策略**: +- 推荐值缓存 24 小时(每天更新一次) +- 市场环境变化时立即更新缓存 +- 用户请求时优先使用缓存,异步更新 + +#### 5.3.8 推荐值参数列表 + +以下参数支持推荐值功能: + +**触发条件参数**: +- 最小获胜概率差异:基于历史回测的最优值 +- 最小交易价值:基于历史回测的最优值 +- 最小剩余时间:基于历史回测的最优值 + +**交易规则参数**: +- 买入金额策略:基于用户策略的推荐 +- 固定买入金额:基于用户策略的推荐 +- 买入比例:基于用户策略的推荐 +- 价格策略:基于市场环境的推荐 +- 价格偏移:基于历史回测的最优值 + +**风险控制参数**: +- 最大持仓:基于用户偏好和账户余额的推荐 +- 最小持仓:基于用户偏好和账户余额的推荐 +- 每日亏损限制:基于用户账户余额的推荐 +- 每日订单限制:基于历史数据的推荐 +- 价格容忍度:基于市场环境的推荐 + +**卖出规则参数**: +- 止盈阈值:基于历史回测的最优值 +- 止损阈值:基于历史回测的最优值 +- 概率反转阈值:基于历史回测的最优值 + +**算法权重参数**(高级): +- 综合评分权重:基于历史回测的最优值 +- 对位分析权重:基于历史回测的最优值 +- 实时状态权重:基于历史回测的最优值 + +--- + +## 六、参数说明和帮助 + +### 6.1 参数说明 + +每个参数都应该有清晰的说明: +- **参数名称**:简洁明了的名称 +- **参数说明**:详细说明参数的作用和影响 +- **取值范围**:明确的范围和单位 +- **推荐值**:基于历史数据的推荐值 +- **示例**:具体的使用示例 + +### 6.2 帮助文档 + +提供以下帮助内容: +- **参数说明文档**:详细的参数说明文档 +- **策略配置指南**:如何配置策略的指南 +- **常见问题**:常见配置问题和解答 +- **最佳实践**:配置策略的最佳实践 + +### 6.3 实时提示 + +在配置过程中提供实时提示: +- **参数影响提示**:说明参数调整对策略的影响 +- **风险提示**:高风险配置的警告提示 +- **优化建议**:基于当前配置的优化建议 + +--- + +**文档结束** + diff --git a/docs/zh/polymarket-nba-markets-fetching-solution.md b/docs/zh/polymarket-nba-markets-fetching-solution.md new file mode 100644 index 0000000..f68dca3 --- /dev/null +++ b/docs/zh/polymarket-nba-markets-fetching-solution.md @@ -0,0 +1,886 @@ +# Polymarket NBA 赛事列表获取方案 + +## 一、概述 + +本文档描述了如何从 Polymarket 获取 NBA 赛事列表,用于量化交易系统中的比赛筛选和市场匹配。 + +--- + +## 二、Polymarket API 接口分析 + +### 2.1 Gamma API 接口 + +**Base URL**: `https://gamma-api.polymarket.com` + +**当前接口**: +- `/markets`: 根据 condition IDs 获取市场信息 +- 不支持直接按分类筛选 + +**接口限制**: +- 只能通过 condition_ids 参数查询特定市场 +- 不支持按分类、标签、关键词等筛选 + +### 2.2 市场数据结构 + +**MarketResponse 字段**: +- `id`: 市场 ID +- `question`: 市场名称(如 "Will the Lakers win?") +- `conditionId`: Condition ID(16 进制) +- `slug`: 市场 slug(用于生成链接) +- `category`: 分类(如 "sports") +- `active`: 是否活跃 +- `closed`: 是否已关闭 +- `archived`: 是否已归档 +- `endDate`: 结束日期 +- `startDate`: 开始日期 +- `outcomes`: 结果选项(JSON 字符串) +- `volume`: 交易量 +- `liquidity`: 流动性 + +--- + +## 三、获取 NBA 赛事列表的方案 + +### 3.1 方案一:通过 Events API 获取(推荐) + +**接口信息**: +- **Base URL**: `https://gamma-api.polymarket.com` +- **接口路径**: `/events` 或 `/series` +- **说明**: Polymarket 可能提供 Events API 或 Series API,可以按分类获取事件列表 + +**实现方式**: +1. 调用 Events API,筛选分类为 "sports" 的事件 +2. 进一步筛选包含 "NBA" 关键词的事件 +3. 获取每个事件关联的市场列表 + +**优点**: +- 可以直接按分类筛选 +- 数据结构更清晰(事件 -> 市场) +- 可以获取事件级别的信息 + +**缺点**: +- 需要确认 API 是否支持分类筛选 +- 可能需要额外的 API 调用 + +### 3.2 方案二:通过搜索接口获取 + +**接口信息**: +- **Base URL**: `https://gamma-api.polymarket.com` +- **接口路径**: `/search` 或 `/markets/search` +- **说明**: 使用搜索接口,通过关键词 "NBA" 搜索市场 + +**实现方式**: +1. 调用搜索接口,搜索关键词 "NBA" +2. 过滤结果,确保分类为 "sports" +3. 进一步过滤,确保市场名称或描述中包含 NBA 相关信息 + +**优点**: +- 可以直接搜索 NBA 相关市场 +- 实现简单 + +**缺点**: +- 可能遗漏一些市场(如果名称不包含 "NBA") +- 搜索结果可能包含不相关的市场 + +### 3.3 方案三:获取所有市场后过滤(备选) + +**实现方式**: +1. 调用 `/markets` 接口,不传 condition_ids(如果支持) +2. 或者定期爬取 Polymarket 网站,获取所有市场 +3. 在本地过滤:分类 = "sports" 且包含 NBA 相关信息 + +**优点**: +- 不依赖特定 API 接口 +- 可以获取完整的数据 + +**缺点**: +- 需要获取大量数据,效率低 +- 需要定期更新 +- 如果 API 不支持获取所有市场,需要爬取网站 + +### 3.4 方案四:通过 Subgraph API 获取(如果可用) + +**接口信息**: +- **Base URL**: Polymarket Subgraph API +- **说明**: 使用 GraphQL 查询,可以灵活筛选 + +**实现方式**: +1. 使用 GraphQL 查询,筛选分类为 "sports" 的市场 +2. 进一步筛选包含 "NBA" 的市场 +3. 可以按时间、状态等条件筛选 + +**优点**: +- 查询灵活,可以精确筛选 +- 可以获取关联数据(如事件、系列等) + +**缺点**: +- 需要确认 Subgraph API 是否可用 +- 需要学习 GraphQL 查询语法 + +--- + +## 四、推荐实现方案 + +### 4.1 混合方案(推荐) + +**策略**:结合多种方式,确保数据完整性 + +**实现步骤**: + +1. **主要方式:Events/Series API** + - 优先使用 Events API 或 Series API + - 按分类 "sports" 筛选 + - 按关键词 "NBA" 筛选事件 + +2. **补充方式:搜索接口** + - 如果 Events API 不可用,使用搜索接口 + - 搜索关键词 "NBA" + - 过滤分类为 "sports" 的结果 + +3. **数据缓存和更新** + - 将获取的 NBA 赛事列表缓存到数据库 + - 定期更新(如每天更新一次) + - 实时监控新市场(通过 WebSocket 或轮询) + +### 4.2 数据存储设计 + +**NBA 赛事表 (nba_games)**: +```sql +CREATE TABLE nba_games ( + 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 '市场描述', + home_team VARCHAR(100) COMMENT '主队名称', + away_team VARCHAR(100) COMMENT '客队名称', + game_date DATE COMMENT '比赛日期', + game_time BIGINT COMMENT '比赛时间(时间戳)', + category VARCHAR(50) DEFAULT 'sports' COMMENT '分类', + active BOOLEAN DEFAULT true COMMENT '是否活跃', + closed BOOLEAN DEFAULT false COMMENT '是否已关闭', + volume VARCHAR(50) COMMENT '交易量', + liquidity VARCHAR(50) COMMENT '流动性', + outcomes TEXT COMMENT '结果选项(JSON)', + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, + INDEX idx_game_date (game_date), + INDEX idx_active (active), + INDEX idx_closed (closed), + INDEX idx_category (category) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='NBA赛事表(Polymarket市场)'; +``` + +### 4.3 数据获取服务实现 + +**服务接口设计**: + +```kotlin +interface NbaMarketService { + /** + * 获取 NBA 赛事列表 + * @param date 比赛日期(可选,不传则获取所有) + * @param activeOnly 是否只获取活跃的市场 + * @return NBA 赛事列表 + */ + suspend fun getNbaMarkets( + date: LocalDate? = null, + activeOnly: Boolean = true + ): Result> + + /** + * 同步 NBA 赛事列表(从 Polymarket API 获取并更新数据库) + * @return 同步结果 + */ + suspend fun syncNbaMarkets(): Result + + /** + * 根据比赛信息匹配 Polymarket 市场 + * @param gameId NBA 比赛 ID + * @param homeTeam 主队名称 + * @param awayTeam 客队名称 + * @param gameDate 比赛日期 + * @return 匹配的市场列表 + */ + suspend fun matchMarketsByGame( + gameId: String, + homeTeam: String, + awayTeam: String, + gameDate: LocalDate + ): Result> +} +``` + +**实现逻辑**: + +1. **从 Polymarket 获取市场列表** + - 调用 Events API 或搜索接口 + - 筛选分类为 "sports" 且包含 "NBA" 的市场 + - 解析市场名称,提取比赛信息(主队、客队、日期等) + +2. **数据解析和匹配** + - 解析市场名称(question),提取球队名称和比赛日期 + - 匹配 NBA 比赛数据(通过球队名称和日期) + - 建立 NBA 比赛和 Polymarket 市场的关联关系 + +3. **数据存储** + - 保存到数据库 + - 建立索引,提高查询效率 + - 定期更新,确保数据最新 + +--- + +## 五、市场名称解析规则 + +### 5.1 市场名称格式 + +Polymarket 的 NBA 市场名称通常包含以下信息: +- 球队名称(主队 vs 客队) +- 比赛日期或时间 +- 比赛结果预测(如 "Will the Lakers win?") + +**常见格式示例**: +- "Will the Lakers beat the Warriors on Dec 15?" +- "Lakers vs Warriors - Dec 15, 2024" +- "NBA: Lakers @ Warriors - Dec 15" + +### 5.2 解析算法 + +**解析步骤**: +1. 提取球队名称:使用正则表达式或关键词匹配 +2. 提取比赛日期:解析日期格式(如 "Dec 15, 2024") +3. 提取比赛结果:判断是 "win" 还是 "lose" +4. 匹配 NBA 比赛:通过球队名称和日期匹配 + +**球队名称映射**: +- 建立球队名称映射表(Polymarket 名称 -> 标准名称) +- 处理缩写(如 "LAL" -> "Lakers") +- 处理别名(如 "Lakers" -> "Los Angeles Lakers") + +--- + +## 六、数据同步策略 + +### 6.1 同步频率 + +**定期同步**: +- 每天同步一次:获取所有 NBA 相关市场 +- 每小时增量同步:检查新市场 +- 实时监控:通过 WebSocket 或轮询监控新市场 + +### 6.2 同步流程 + +1. **全量同步**(每天一次) + - 从 Polymarket API 获取所有 sports 分类的市场 + - 筛选包含 NBA 相关信息的市场 + - 更新数据库 + +2. **增量同步**(每小时一次) + - 获取最近 24 小时的新市场 + - 只更新新增和变化的市场 + - 标记已关闭的市场 + +3. **实时监控**(可选) + - 通过 WebSocket 订阅新市场 + - 或每 5 分钟轮询一次新市场 + - 实时更新数据库 + +### 6.3 数据去重和更新 + +**去重策略**: +- 使用 condition_id 作为唯一标识 +- 如果市场已存在,更新信息 +- 如果市场不存在,插入新记录 + +**更新策略**: +- 更新活跃状态(active、closed) +- 更新交易量和流动性 +- 更新结束日期(如果变化) + +--- + +## 七、API 接口扩展 + +### 7.1 扩展 Gamma API 接口 + +**添加新的接口方法**: + +```kotlin +interface PolymarketGammaApi { + // ... 现有方法 + + /** + * 搜索市场(如果 API 支持) + * @param query 搜索关键词 + * @param category 分类筛选 + * @param limit 返回数量限制 + * @param offset 偏移量 + * @return 市场列表 + */ + @GET("/markets/search") + suspend fun searchMarkets( + @Query("query") query: String? = null, + @Query("category") category: String? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null + ): Response> + + /** + * 获取事件列表(如果 API 支持) + * @param category 分类筛选 + * @param limit 返回数量限制 + * @param offset 偏移量 + * @return 事件列表 + */ + @GET("/events") + suspend fun listEvents( + @Query("category") category: String? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null + ): Response> + + /** + * 获取系列列表(如果 API 支持) + * @param category 分类筛选 + * @param limit 返回数量限制 + * @param offset 偏移量 + * @return 系列列表 + */ + @GET("/series") + suspend fun listSeries( + @Query("category") category: String? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null + ): Response> +} +``` + +### 7.2 后端服务实现 + +**创建 NBA 市场服务**: + +```kotlin +@Service +class NbaMarketService( + private val gammaApi: PolymarketGammaApi, + private val nbaGameRepository: NbaGameRepository +) { + /** + * 获取 NBA 赛事列表 + */ + suspend fun getNbaMarkets( + date: LocalDate? = null, + activeOnly: Boolean = true + ): Result> { + // 实现逻辑 + } + + /** + * 同步 NBA 赛事列表 + */ + suspend fun syncNbaMarkets(): Result { + // 实现逻辑 + } + + /** + * 匹配市场 + */ + suspend fun matchMarketsByGame( + gameId: String, + homeTeam: String, + awayTeam: String, + gameDate: LocalDate + ): Result> { + // 实现逻辑 + } +} +``` + +### 7.3 前端 API 接口 + +**添加市场列表接口**: + +```kotlin +@RestController +@RequestMapping("/api/nba/markets") +class NbaMarketController( + private val nbaMarketService: NbaMarketService +) { + /** + * 获取 NBA 赛事列表 + */ + @PostMapping("/list") + fun getNbaMarkets(@RequestBody request: NbaMarketListRequest): ResponseEntity>> { + // 实现逻辑 + } + + /** + * 同步 NBA 赛事列表 + */ + @PostMapping("/sync") + fun syncNbaMarkets(): ResponseEntity> { + // 实现逻辑 + } + + /** + * 根据比赛匹配市场 + */ + @PostMapping("/match") + fun matchMarkets(@RequestBody request: MatchMarketRequest): ResponseEntity>> { + // 实现逻辑 + } +} +``` + +--- + +## 八、实施步骤 + +### 8.1 第一阶段:API 调研和测试(1 周) + +**任务清单**: +- [ ] 调研 Polymarket API 文档,确认可用的接口 +- [ ] 测试 Events API 或 Series API(如果存在) +- [ ] 测试搜索接口(如果存在) +- [ ] 确认最佳的数据获取方式 + +### 8.2 第二阶段:数据获取实现(1-2 周) + +**任务清单**: +- [ ] 扩展 PolymarketGammaApi 接口 +- [ ] 实现 NBA 市场数据获取服务 +- [ ] 实现市场名称解析算法 +- [ ] 实现 NBA 比赛和市场匹配逻辑 + +### 8.3 第三阶段:数据存储和同步(1 周) + +**任务清单**: +- [ ] 创建数据库表(nba_games) +- [ ] 实现数据存储逻辑 +- [ ] 实现数据同步服务 +- [ ] 实现定时同步任务 + +### 8.4 第四阶段:API 接口开发(1 周) + +**任务清单**: +- [ ] 创建后端 API 接口 +- [ ] 实现前端 API 调用 +- [ ] 实现前端页面展示 +- [ ] 测试和优化 + +--- + +## 九、注意事项 + +### 9.1 API 限制 + +- **请求频率**:注意 API 的请求频率限制 +- **数据量**:NBA 相关市场可能很多,需要分页获取 +- **数据更新**:市场状态可能频繁变化,需要定期更新 + +### 9.2 数据匹配 + +- **球队名称**:Polymarket 的球队名称可能与标准名称不一致,需要建立映射表 +- **比赛日期**:需要处理时区问题 +- **市场状态**:需要实时更新市场的活跃状态 + +### 9.3 错误处理 + +- **API 失败**:实现重试机制 +- **数据解析失败**:记录错误日志,跳过无法解析的市场 +- **匹配失败**:如果无法匹配 NBA 比赛,仍然保存市场数据,后续可以手动匹配 + +--- + +## 十、实际实现方案(基于当前 API 限制) + +### 10.1 当前 API 限制分析 + +**现状**: +- Polymarket Gamma API 的 `/markets` 接口只支持通过 `condition_ids` 查询 +- 不支持按分类、关键词、日期等筛选 +- 没有提供搜索接口或 Events API + +**解决方案**: +由于 API 限制,需要采用以下策略: +1. **建立 NBA 市场 condition_ids 数据库**:手动或通过其他方式收集 NBA 市场的 condition_ids +2. **定期更新 condition_ids 列表**:通过爬取或监控获取新的 NBA 市场 +3. **批量查询市场信息**:使用收集到的 condition_ids 批量查询市场详情 + +### 10.2 实现步骤 + +#### 步骤 1:收集 NBA 市场 condition_ids + +**方式一:手动收集** +- 访问 Polymarket 网站,搜索 "NBA" 相关市场 +- 从市场 URL 中提取 condition_id(如 `https://polymarket.com/event/xxx`) +- 保存到数据库或配置文件 + +**方式二:通过爬取获取** +- 爬取 Polymarket 网站的市场列表页面 +- 解析 HTML,提取 condition_ids +- 筛选分类为 "sports" 且包含 "NBA" 的市场 + +**方式三:通过监控获取** +- 监控 Polymarket 的新市场创建 +- 通过 WebSocket 或轮询获取新市场 +- 筛选 NBA 相关市场 + +#### 步骤 2:批量查询市场信息 + +**实现代码**: + +```kotlin +@Service +class NbaMarketService( + private val gammaApi: PolymarketGammaApi, + private val nbaGameRepository: NbaGameRepository +) { + private val logger = LoggerFactory.getLogger(NbaMarketService::class.java) + + /** + * 批量获取 NBA 市场信息 + * @param conditionIds condition ID 列表 + * @return 市场列表 + */ + suspend fun batchGetMarkets(conditionIds: List): Result> { + return try { + // Polymarket API 可能对批量查询有限制,需要分批查询 + val batchSize = 50 // 每批查询 50 个 + val allMarkets = mutableListOf() + + conditionIds.chunked(batchSize).forEach { batch -> + val response = gammaApi.listMarkets( + conditionIds = batch, + includeTag = true + ) + + if (response.isSuccessful && response.body() != null) { + val markets = response.body()!! + // 过滤 NBA 相关市场 + val nbaMarkets = markets.filter { market -> + isNbaMarket(market) + } + allMarkets.addAll(nbaMarkets) + } else { + logger.warn("批量查询市场失败: ${response.code()} ${response.message()}") + } + + // 避免请求过快,添加延迟 + kotlinx.coroutines.delay(100) + } + + Result.success(allMarkets) + } catch (e: Exception) { + logger.error("批量获取市场信息异常: ${e.message}", e) + Result.failure(e) + } + } + + /** + * 判断是否为 NBA 市场 + */ + private fun isNbaMarket(market: MarketResponse): Boolean { + // 检查分类 + if (market.category?.lowercase() != "sports") { + return false + } + + // 检查市场名称或描述中是否包含 NBA 相关关键词 + val question = market.question?.lowercase() ?: "" + val description = market.description?.lowercase() ?: "" + + val nbaKeywords = listOf( + "nba", "basketball", "lakers", "warriors", "celtics", "heat", + "bulls", "knicks", "nets", "76ers", "bucks", "suns", "nuggets" + ) + + return nbaKeywords.any { keyword -> + question.contains(keyword) || description.contains(keyword) + } + } + + /** + * 同步 NBA 市场列表 + */ + suspend fun syncNbaMarkets(): Result { + return try { + // 1. 从数据库获取所有已知的 condition_ids + val knownConditionIds = nbaGameRepository.findAllConditionIds() + + // 2. 批量查询市场信息 + val marketsResult = batchGetMarkets(knownConditionIds) + + if (!marketsResult.isSuccess) { + return Result.failure(marketsResult.exceptionOrNull() ?: Exception("获取市场失败")) + } + + val markets = marketsResult.getOrNull() ?: emptyList() + + // 3. 解析市场信息,提取比赛数据 + val nbaGames = markets.mapNotNull { market -> + parseMarketToNbaGame(market) + } + + // 4. 保存到数据库 + nbaGameRepository.saveAll(nbaGames) + + Result.success( + SyncResult( + total = markets.size, + success = nbaGames.size, + failed = markets.size - nbaGames.size + ) + ) + } catch (e: Exception) { + logger.error("同步 NBA 市场列表异常: ${e.message}", e) + Result.failure(e) + } + } + + /** + * 解析市场信息为 NBA 比赛数据 + */ + private fun parseMarketToNbaGame(market: MarketResponse): NbaGame? { + return try { + // 解析市场名称,提取球队名称和日期 + val (homeTeam, awayTeam, gameDate) = parseMarketQuestion(market.question ?: "") + ?: return null + + NbaGame( + polymarketMarketId = market.id, + conditionId = market.conditionId ?: return null, + marketSlug = market.slug, + marketQuestion = market.question, + marketDescription = market.description, + homeTeam = homeTeam, + awayTeam = awayTeam, + gameDate = gameDate, + gameTime = parseGameTime(market.startDate), + category = market.category ?: "sports", + active = market.active ?: true, + closed = market.closed ?: false, + volume = market.volume, + liquidity = market.liquidity, + outcomes = market.outcomes, + createdAt = System.currentTimeMillis(), + updatedAt = System.currentTimeMillis() + ) + } catch (e: Exception) { + logger.warn("解析市场信息失败: ${market.question}, error: ${e.message}") + null + } + } + + /** + * 解析市场名称,提取球队名称和日期 + * 示例: "Will the Lakers beat the Warriors on Dec 15?" -> ("Lakers", "Warriors", 2024-12-15) + */ + private fun parseMarketQuestion(question: String): Triple? { + // 实现解析逻辑 + // 使用正则表达式或 NLP 方法提取信息 + // 这里简化处理,实际需要更复杂的解析逻辑 + return null + } +} +``` + +#### 步骤 3:定时同步任务 + +**实现代码**: + +```kotlin +@Component +class NbaMarketSyncScheduler( + private val nbaMarketService: NbaMarketService +) { + private val logger = LoggerFactory.getLogger(NbaMarketSyncScheduler::class.java) + + /** + * 每天凌晨 2 点同步一次 + */ + @Scheduled(cron = "0 0 2 * * ?") + fun syncNbaMarketsDaily() { + logger.info("开始同步 NBA 市场列表...") + runBlocking { + val result = nbaMarketService.syncNbaMarkets() + result.fold( + onSuccess = { syncResult -> + logger.info("同步 NBA 市场列表成功: total=${syncResult.total}, success=${syncResult.success}, failed=${syncResult.failed}") + }, + onFailure = { e -> + logger.error("同步 NBA 市场列表失败: ${e.message}", e) + } + ) + } + } + + /** + * 每小时增量同步一次 + */ + @Scheduled(cron = "0 0 * * * ?") + fun syncNbaMarketsHourly() { + logger.info("开始增量同步 NBA 市场列表...") + runBlocking { + // 只同步最近 24 小时的新市场 + val result = nbaMarketService.syncNbaMarketsIncremental() + result.fold( + onSuccess = { syncResult -> + logger.info("增量同步 NBA 市场列表成功: total=${syncResult.total}, success=${syncResult.success}") + }, + onFailure = { e -> + logger.error("增量同步 NBA 市场列表失败: ${e.message}", e) + } + ) + } + } +} +``` + +### 10.3 数据模型定义 + +**实体类**: + +```kotlin +@Entity +@Table(name = "nba_games") +data class NbaGame( + @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 = "home_team", length = 100) + val homeTeam: String? = null, + + @Column(name = "away_team", length = 100) + val awayTeam: String? = null, + + @Column(name = "game_date") + val gameDate: LocalDate? = null, + + @Column(name = "game_time") + val gameTime: Long? = 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 = "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 = "created_at", nullable = false) + val createdAt: Long = System.currentTimeMillis(), + + @Column(name = "updated_at", nullable = false) + var updatedAt: Long = System.currentTimeMillis() +) +``` + +### 10.4 API 接口实现 + +**Controller**: + +```kotlin +@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>> { + return try { + val result = runBlocking { + nbaMarketService.getNbaMarkets( + date = request.date, + activeOnly = request.activeOnly ?: true + ) + } + + result.fold( + onSuccess = { markets -> + ResponseEntity.ok(ApiResponse.success(markets)) + }, + 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("/sync") + fun syncNbaMarkets(): ResponseEntity> { + return try { + val result = runBlocking { + nbaMarketService.syncNbaMarkets() + } + + result.fold( + onSuccess = { syncResult -> + ResponseEntity.ok(ApiResponse.success(syncResult)) + }, + 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)) + } + } +} +``` + +--- + +## 十一、参考资源 + +- [Polymarket API 文档](https://docs.polymarket.com/) +- [Polymarket Gamma API 文档](https://docs.polymarket.com/api-reference/markets/list-markets) +- [NBA 球队名称标准](https://www.nba.com/teams) +- [Polymarket 网站](https://polymarket.com/)(用于手动收集 condition_ids) + +--- + +**文档结束** + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8f6f9eb..ba25cda 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,6 +33,11 @@ import FilteredOrdersList from './pages/FilteredOrdersList' import SystemSettings from './pages/SystemSettings' import ApiHealthStatus from './pages/ApiHealthStatus' import Announcements from './pages/Announcements' +import NbaQuantitativeStrategyList from './pages/NbaQuantitativeStrategyList' +import NbaQuantitativeStrategyAdd from './pages/NbaQuantitativeStrategyAdd' +import NbaQuantitativeStrategyEdit from './pages/NbaQuantitativeStrategyEdit' +import NbaTradingSignals from './pages/NbaTradingSignals' +import NbaStatistics from './pages/NbaStatistics' import { wsManager } from './services/websocket' import type { OrderPushMessage } from './types' import { apiService } from './services/api' @@ -259,6 +264,11 @@ function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index d0df2ac..a94a2a7 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -19,7 +19,9 @@ import { TwitterOutlined, CheckCircleOutlined, SendOutlined, - NotificationOutlined + NotificationOutlined, + ThunderboltOutlined, + SignalFilled } from '@ant-design/icons' import type { MenuProps } from 'antd' import type { ReactNode } from 'react' @@ -52,6 +54,9 @@ const Layout: React.FC = ({ children }) => { if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) { keys.push('/copy-trading-management') } + if (path.startsWith('/nba')) { + keys.push('/nba-quantitative-trading') + } if (path.startsWith('/system-settings')) { keys.push('/system-settings') } @@ -67,6 +72,9 @@ const Layout: React.FC = ({ children }) => { if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) { keys.push('/copy-trading-management') } + if (path.startsWith('/nba')) { + keys.push('/nba-quantitative-trading') + } if (path.startsWith('/system-settings')) { keys.push('/system-settings') } @@ -111,6 +119,23 @@ const Layout: React.FC = ({ children }) => { icon: , label: t('menu.positions') }, + { + key: '/nba-quantitative-trading', + icon: , + label: t('menu.nbaQuantitativeTrading') || 'NBA量化交易', + children: [ + { + key: '/nba/strategies', + icon: , + label: t('menu.nbaStrategies') || '策略管理' + }, + { + key: '/nba/signals', + icon: , + label: t('menu.nbaSignals') || '交易信号' + } + ] + }, { key: '/statistics', icon: , @@ -169,7 +194,7 @@ const Layout: React.FC = ({ children }) => { const handleMenuClick = ({ key }: { key: string }) => { // 如果是父菜单,不导航(但 /system-settings 作为子菜单项时可以导航) - if (key === '/copy-trading-management') { + if (key === '/copy-trading-management' || key === '/nba-quantitative-trading') { return } diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index bae7339..0b1325a 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -181,7 +181,10 @@ "logout": "退出登录", "logoutConfirm": "确认退出", "logoutConfirmDesc": "确定要退出登录吗?", - "navigation": "导航菜单" + "navigation": "导航菜单", + "nbaQuantitativeTrading": "NBA量化交易", + "nbaStrategies": "策略管理", + "nbaSignals": "交易信号" }, "apiHealthStatus": { "title": "API 健康状态", diff --git a/frontend/src/pages/NbaQuantitativeStrategyAdd.tsx b/frontend/src/pages/NbaQuantitativeStrategyAdd.tsx new file mode 100644 index 0000000..b215ae2 --- /dev/null +++ b/frontend/src/pages/NbaQuantitativeStrategyAdd.tsx @@ -0,0 +1,828 @@ +import { useEffect, useState, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import { Card, Form, Button, Steps, message, Input, Select, Switch, InputNumber, DatePicker, Space, Divider, Checkbox } from 'antd' +import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons' +import { apiService } from '../services/api' +import { useAccountStore } from '../store/accountStore' +import type { NbaQuantitativeStrategyCreateRequest, NbaGame } from '../types' +import { useTranslation } from 'react-i18next' +import { useMediaQuery } from 'react-responsive' +import dayjs, { Dayjs } from 'dayjs' +import utc from 'dayjs/plugin/utc' +import timezone from 'dayjs/plugin/timezone' + +// 配置 dayjs 时区插件 +dayjs.extend(utc) +dayjs.extend(timezone) + +const { Option } = Select +const { TextArea } = Input +const { RangePicker } = DatePicker + +const NbaQuantitativeStrategyAdd: React.FC = () => { + const { t } = useTranslation() + const navigate = useNavigate() + const isMobile = useMediaQuery({ maxWidth: 768 }) + const { accounts, fetchAccounts } = useAccountStore() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [currentStep, setCurrentStep] = useState(0) + const [games, setGames] = useState([]) + const [loadingGames, setLoadingGames] = useState(false) + const [selectedGameId, setSelectedGameId] = useState(null) + + const fetchGames = useCallback(async () => { + setLoadingGames(true) + try { + // 使用西8区时间计算时间戳 + const today = dayjs().tz('America/Los_Angeles').startOf('day') + const nextWeek = dayjs().tz('America/Los_Angeles').add(7, 'day').endOf('day') + + const response = await apiService.nbaGames.list({ + startTimestamp: today.valueOf(), // 传递时间戳(毫秒) + endTimestamp: nextWeek.valueOf() // 传递时间戳(毫秒) + }) + if (response.data.code === 0 && response.data.data) { + setGames(response.data.data.list || []) + } else { + message.warning('获取比赛列表失败,请稍后重试') + } + } catch (error: any) { + message.error(error.message || '获取比赛列表失败') + } finally { + setLoadingGames(false) + } + }, []) + + useEffect(() => { + fetchAccounts() + fetchGames() + // 设置默认值 + form.setFieldsValue({ + enabled: true, + minWinProbabilityDiff: 0.1, + minTradeValue: 0.05, + buyAmountStrategy: 'FIXED', + fixedBuyAmount: 10, + buyTiming: 'IMMEDIATE', + buyDirection: 'AUTO', + enableSell: true, + sellRatio: 1.0, + sellTiming: 'IMMEDIATE', + priceStrategy: 'MARKET', + priceOffset: 0, + maxPosition: 50, + minPosition: 5, + priceTolerance: 0.05, + baseStrengthWeight: 0.3, + recentFormWeight: 0.25, + lineupIntegrityWeight: 0.2, + starStatusWeight: 0.15, + environmentWeight: 0.1, + matchupAdvantageWeight: 0.2, + scoreDiffWeight: 0.3, + momentumWeight: 0.2, + dataUpdateFrequency: 30, + analysisFrequency: 30, + pushFailedOrders: false, + pushFrequency: 'REALTIME', + batchPushInterval: 1 + }) + }, [fetchGames]) + + const handleGameSelectionChange = (gameId: string | null) => { + setSelectedGameId(gameId) + if (gameId) { + const selectedGame = games.find(game => game.nbaGameId === gameId) + if (selectedGame) { + // 自动提取该比赛的两支球队 + form.setFieldsValue({ + filterTeams: [selectedGame.homeTeam, selectedGame.awayTeam] + }) + } + } else { + form.setFieldsValue({ + filterTeams: undefined + }) + } + } + + const steps = [ + { title: '基本信息', description: '策略名称和账户' }, + { title: '触发条件', description: '概率阈值和交易价值' }, + { title: '交易规则', description: '买入卖出规则' }, + { title: '风险控制', description: '持仓和每日限制' }, + { title: '高级配置', description: '算法权重和系统配置' } + ] + + const handleSubmit = async () => { + try { + const values = await form.validateFields() + setLoading(true) + + const request: NbaQuantitativeStrategyCreateRequest = { + strategyName: values.strategyName, + strategyDescription: values.strategyDescription, + accountId: values.accountId, + enabled: values.enabled, + filterTeams: values.filterTeams, + filterDateFrom: values.dateRange?.[0]?.format('YYYY-MM-DD'), + filterDateTo: values.dateRange?.[1]?.format('YYYY-MM-DD'), + filterGameImportance: values.filterGameImportance, + minWinProbabilityDiff: values.minWinProbabilityDiff?.toString(), + minWinProbability: values.minWinProbability?.toString(), + maxWinProbability: values.maxWinProbability?.toString(), + minTradeValue: values.minTradeValue?.toString(), + minRemainingTime: values.minRemainingTime, + maxRemainingTime: values.maxRemainingTime, + minScoreDiff: values.minScoreDiff, + maxScoreDiff: values.maxScoreDiff, + buyAmountStrategy: values.buyAmountStrategy, + fixedBuyAmount: values.fixedBuyAmount?.toString(), + buyRatio: values.buyRatio?.toString(), + baseBuyAmount: values.baseBuyAmount?.toString(), + buyTiming: values.buyTiming, + delayBuySeconds: values.delayBuySeconds, + buyDirection: values.buyDirection, + enableSell: values.enableSell, + takeProfitThreshold: values.takeProfitThreshold?.toString(), + stopLossThreshold: values.stopLossThreshold?.toString(), + probabilityReversalThreshold: values.probabilityReversalThreshold?.toString(), + sellRatio: values.sellRatio?.toString(), + sellTiming: values.sellTiming, + delaySellSeconds: values.delaySellSeconds, + priceStrategy: values.priceStrategy, + fixedPrice: values.fixedPrice?.toString(), + priceOffset: values.priceOffset?.toString(), + maxPosition: values.maxPosition?.toString(), + minPosition: values.minPosition?.toString(), + maxGamePosition: values.maxGamePosition?.toString(), + maxDailyLoss: values.maxDailyLoss?.toString(), + maxDailyOrders: values.maxDailyOrders, + maxDailyProfit: values.maxDailyProfit?.toString(), + priceTolerance: values.priceTolerance?.toString(), + minProbabilityThreshold: values.minProbabilityThreshold?.toString(), + maxProbabilityThreshold: values.maxProbabilityThreshold?.toString(), + baseStrengthWeight: values.baseStrengthWeight?.toString(), + recentFormWeight: values.recentFormWeight?.toString(), + lineupIntegrityWeight: values.lineupIntegrityWeight?.toString(), + starStatusWeight: values.starStatusWeight?.toString(), + environmentWeight: values.environmentWeight?.toString(), + matchupAdvantageWeight: values.matchupAdvantageWeight?.toString(), + scoreDiffWeight: values.scoreDiffWeight?.toString(), + momentumWeight: values.momentumWeight?.toString(), + dataUpdateFrequency: values.dataUpdateFrequency, + analysisFrequency: values.analysisFrequency, + pushFailedOrders: values.pushFailedOrders, + pushFrequency: values.pushFrequency, + batchPushInterval: values.batchPushInterval + } + + const response = await apiService.nbaStrategies.create(request) + if (response.data.code === 0) { + message.success('创建策略成功') + navigate('/nba/strategies') + } else { + message.error(response.data.msg || '创建策略失败') + } + } catch (error: any) { + if (error.errorFields) { + // 表单验证错误 + const firstErrorField = error.errorFields[0] + message.error(`${firstErrorField.name.join('.')}: ${firstErrorField.errors[0]}`) + } else { + message.error(error.message || '创建策略失败') + } + } finally { + setLoading(false) + } + } + + const next = async () => { + try { + const fields = getFieldsForStep(currentStep) + await form.validateFields(fields) + setCurrentStep(currentStep + 1) + } catch (error) { + // 验证失败,不跳转 + } + } + + const prev = () => { + setCurrentStep(currentStep - 1) + } + + const getFieldsForStep = (step: number): string[] => { + switch (step) { + case 0: + return ['strategyName', 'accountId'] + case 1: + return ['minWinProbabilityDiff', 'minTradeValue'] + case 2: + return ['buyAmountStrategy', 'priceStrategy'] + case 3: + return ['maxPosition', 'minPosition'] + default: + return [] + } + } + + const buyAmountStrategy = Form.useWatch('buyAmountStrategy', form) + const priceStrategy = Form.useWatch('priceStrategy', form) + const buyTiming = Form.useWatch('buyTiming', form) + const sellTiming = Form.useWatch('sellTiming', form) + const pushFrequency = Form.useWatch('pushFrequency', form) + + return ( +
+ + + + + + + +
+ {/* 第一步:基本信息 */} + {currentStep === 0 && ( + <> + + + + + +