From a3292c3dd9bd5847358dea752ba28c732bece653 Mon Sep 17 00:00:00 2001 From: mianyang <784310601@qq.com> Date: Sun, 17 May 2026 13:19:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=8C=E6=9D=BF=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=A7=92=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api-overview.md | 4 +- docs/second-board-profit-summary.md | 230 ++++++++++++++++ internal/api/second_board_profit_handler.go | 67 +++++ internal/model/limit_board.go | 53 ++++ internal/router/router.go | 2 + .../service/second_board_profit_service.go | 257 ++++++++++++++++++ 6 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 docs/second-board-profit-summary.md create mode 100644 internal/api/second_board_profit_handler.go create mode 100644 internal/service/second_board_profit_service.go diff --git a/docs/api-overview.md b/docs/api-overview.md index f583e42..82f7347 100644 --- a/docs/api-overview.md +++ b/docs/api-overview.md @@ -4,7 +4,7 @@ - 服务前缀:`/api/v1` - 响应格式:`application/json` -- 当前项目接口总数:11 +- 当前项目接口总数:13 ## 2. 接口列表 @@ -17,6 +17,8 @@ | 涨停低开分组 | `GET` | `/api/v1/limit-board/open-lower-groups` | 获取查询日和前一交易日均涨停的股票,并按是否低于前一交易日收盘价分组 | [limit-board-open-lower-groups.md](./limit-board-open-lower-groups.md) | | 涨停开盘阈值分组 | `GET` | `/api/v1/limit-board/open-threshold` | 获取查询日和前一交易日均涨停的股票,并按开盘价是否低于前一交易日收盘价 101% 分组 | [limit-board-open-threshold.md](./limit-board-open-threshold.md) | | 二板早盘次日盈利 | `GET` | `/api/v1/limit-board/second-board-profit` | 分页查询 T-2 无涨停、T-1 涨停、T 涨停的次日盈利明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) | +| 二板获利综合 | `GET` | `/api/v1/limit-board/second-board-profit-summary` | 汇总每只个股 2020 年至今 09:31:00-09:59:59 二板后的次日最大获利,并按累计获利倒序排序 | [second-board-profit-summary.md](./second-board-profit-summary.md) | +| 个股二板获利详情 | `GET` | `/api/v1/limit-board/second-board-profit-detail` | 查询单只个股每次 09:31:00-09:59:59 二板后的次日最大获利 | [second-board-profit-summary.md](./second-board-profit-summary.md) | | 二板日历 | `GET` | `/api/v1/limit-board/second-cal` | 按交易日统计二板数量、封板/炸板数量和 T+1 盘中盈利概率 | [second-cal.md](./second-cal.md) | | 炸板次日表现 | `GET` | `/api/v1/limit-board/break-next-performance` | 分页查询 T-2 未涨停、T-1 涨停、T 日炸板股票及 T+1 表现 | [limit-board-break-next-performance.md](./limit-board-break-next-performance.md) | | 下载 | `GET` | `/api/v1/download/limit-board/second-board-profit-csv` | 下载筛选条件范围内的二板早盘次日盈利 CSV 明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) | diff --git a/docs/second-board-profit-summary.md b/docs/second-board-profit-summary.md new file mode 100644 index 0000000..1059b5f --- /dev/null +++ b/docs/second-board-profit-summary.md @@ -0,0 +1,230 @@ +# 二板获利综合与个股详情接口文档 + +## 1. 接口说明 + +本页包含两个二板获利统计接口: + +- 二板获利综合:汇总每只个股 2020 年至今早盘二板后的 T+1 最大获利,并按累计获利从高到低排序。 +- 个股二板获利详情:查询单只个股每次早盘二板后的 T+1 最大获利情况。 + +接口当前读取缓存表,不再在请求时实时联表计算: + +- `second_board_profit_detail_cache`:保存每次命中的二板详情和 T+1 最大获利。 +- `second_board_profit_summary_cache`:保存完整缓存范围内按个股聚合后的综合数据。 + +业务筛选基础条件: + +- T-2 交易日无涨停。 +- T-1 交易日涨停。 +- T 交易日再次触及涨停价,T 日可为封板或炸板。 +- T 日二板首次涨停时间在 `09:31:00` 到 `09:59:59` 之间,包含起止时间。 + +## 2. 接口地址 + +| 接口名称 | 请求方式 | 请求路径 | 说明 | +| --- | --- | --- | --- | +| 二板获利综合 | `GET` | `/api/v1/limit-board/second-board-profit-summary` | 汇总每只个股 2020 年至今 09:31:00-09:59:59 二板后的次日最大获利 | +| 个股二板获利详情 | `GET` | `/api/v1/limit-board/second-board-profit-detail` | 查询单只个股每次 09:31:00-09:59:59 二板后的次日最大获利 | + +## 3. 统计规则 + +- 统计口径复用二板日历接口:T-2 未涨停、T-1 涨停、T 日再次触及涨停价。 +- T+1 最大获利百分比为 `next_high_profit_pct`,按 `(T+1最高价 - T日最高价) / T日最高价 * 100` 计算并保留两位小数。 +- 二板获利综合接口按个股累计 `next_high_profit_pct`,得到 `total_next_high_profit_pct`,并按该字段从高到低排序。 +- 没有 T+1 最高价或 T 日最高价的数据仍会计入二板次数,但不参与获利累计和平均值计算。 +- 当请求日期范围覆盖整张明细缓存表时,二板获利综合接口直接读取 `second_board_profit_summary_cache`。 +- 当请求日期范围小于明细缓存表覆盖范围时,二板获利综合接口从 `second_board_profit_detail_cache` 按日期范围动态聚合。 +- 个股二板获利详情接口始终从 `second_board_profit_detail_cache` 按日期范围和股票代码查询。 + +## 4. 缓存刷新 + +缓存表需要由离线脚本或人工任务刷新。接口只返回缓存表中已经落地的数据;如果最新交易日或历史补数未写入缓存表,接口不会临时回退到实时联表计算。 + +当前校验结果: + +- `second_board_profit_detail_cache` 当前覆盖 `2020-01-06` 到 `2026-05-14`。 +- 在该覆盖范围内,缓存明细和原实时计算口径数量一致、键一致、字段一致。 +- 默认查询范围 `2020-01-01` 到当天如果覆盖了缓存表之外的日期,需要先刷新缓存表,否则接口只返回已落地缓存数据。 + +## 5. 请求参数 + +两个接口都支持以下日期参数: + +| 参数名 | 类型 | 必填 | 默认值 | 示例 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `tradeStartDate` | `string` | 否 | `2020-01-01` | `2020-01-01` | T 日交易开始日期,格式 `YYYY-MM-DD` | +| `tradeEndDate` | `string` | 否 | 当天 | `2026-05-16` | T 日交易结束日期,格式 `YYYY-MM-DD` | + +个股二板获利详情接口还需要传入股票代码,`tsCode` 和 `symbol` 二选一: + +| 参数名 | 类型 | 必填 | 默认值 | 示例 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `tsCode` | `string` | 否 | 空 | `000001.SZ` | TuShare 股票代码,优先使用 | +| `symbol` | `string` | 否 | 空 | `000001` | 市场展示股票代码;未传 `tsCode` 时使用 | + +兼容说明:日期参数兼容 `trade_start_date`、`start_date`、`trade_end_date`、`end_date`;`symbol` 兼容别名 `code`。 + +## 6. 请求示例 + +二板获利综合: + +```http +GET /api/v1/limit-board/second-board-profit-summary +``` + +指定日期范围的二板获利综合: + +```http +GET /api/v1/limit-board/second-board-profit-summary?tradeStartDate=2020-01-01&tradeEndDate=2026-05-16 +``` + +个股二板获利详情: + +```http +GET /api/v1/limit-board/second-board-profit-detail?tsCode=000001.SZ +``` + +## 7. 二板获利综合响应 + +| 字段名 | 类型 | 说明 | +| --- | --- | --- | +| `trade_start_date` | `string` | 查询起始 T 日 | +| `trade_end_date` | `string` | 查询结束 T 日 | +| `start_time` | `string` | 固定筛选起点,当前为 `09:31:00` | +| `end_time` | `string` | 固定筛选终点,当前为 `09:59:59` | +| `total` | `int` | 命中的个股数量 | +| `list` | `array` | 按累计获利从高到低排序的个股汇总数据 | + +`list[]` 字段: + +| 字段名 | 类型 | 说明 | +| --- | --- | --- | +| `symbol` | `string` | 股票代码 | +| `ts_code` | `string` | TuShare 股票代码 | +| `name` | `string` | 股票简称 | +| `market` | `string` | 所属板块 | +| `industry` | `string` | 所属行业 | +| `second_board_count` | `int` | 09:31:00-09:59:59 期间命中的二板次数 | +| `limit_count` | `int` | 二板封板次数 | +| `break_count` | `int` | 二板炸板次数 | +| `profit_data_count` | `int` | 具备 T+1 最大获利百分比的次数 | +| `total_next_high_profit_pct` | `number` | T+1 最大获利百分比累计值 | +| `average_next_high_profit_pct` | `number` | T+1 最大获利百分比平均值 | +| `first_trade_date` | `string` | 首次命中交易日 | +| `last_trade_date` | `string` | 最近命中交易日 | + +响应示例: + +```json +{ + "trade_start_date": "2020-01-01", + "trade_end_date": "2026-05-16", + "start_time": "09:31:00", + "end_time": "09:59:59", + "total": 1, + "list": [ + { + "symbol": "000001", + "ts_code": "000001.SZ", + "name": "示例股份", + "market": "主板", + "industry": "银行", + "second_board_count": 3, + "limit_count": 2, + "break_count": 1, + "profit_data_count": 3, + "total_next_high_profit_pct": 12.36, + "average_next_high_profit_pct": 4.12, + "first_trade_date": "2021-03-18", + "last_trade_date": "2026-04-24" + } + ] +} +``` + +## 8. 个股二板获利详情响应 + +| 字段名 | 类型 | 说明 | +| --- | --- | --- | +| `trade_start_date` | `string` | 查询起始 T 日 | +| `trade_end_date` | `string` | 查询结束 T 日 | +| `start_time` | `string` | 固定筛选起点,当前为 `09:31:00` | +| `end_time` | `string` | 固定筛选终点,当前为 `09:59:59` | +| `symbol` | `string` | 股票代码 | +| `ts_code` | `string` | TuShare 股票代码 | +| `name` | `string` | 股票简称 | +| `market` | `string` | 所属板块 | +| `industry` | `string` | 所属行业 | +| `second_board_count` | `int` | 命中的二板次数 | +| `profit_data_count` | `int` | 具备 T+1 最大获利百分比的次数 | +| `total_next_high_profit_pct` | `number` | T+1 最大获利百分比累计值 | +| `average_next_high_profit_pct` | `number` | T+1 最大获利百分比平均值 | +| `list` | `array` | 每次二板获利详情,字段同二板日历接口 `SecondCalStock` | + +`list[]` 关键字段: + +| 字段名 | 类型 | 说明 | +| --- | --- | --- | +| `trade_date` | `string` | T 日交易日 | +| `previous_trade_date` | `string` | T-1 交易日 | +| `previous_two_trade_date` | `string` | T-2 交易日 | +| `next_trade_date` | `string` | T+1 交易日 | +| `status` | `string` | `limit` 表示封板,`break` 表示炸板 | +| `first_limit_time` | `string` | T 日二板首次涨停时间,固定落在 `09:31:00` 到 `09:59:59` | +| `t_high` | `number \| null` | T 日最高价 | +| `next_high` | `number \| null` | T+1 最高价 | +| `next_intraday_profitable` | `boolean \| null` | T+1 最高价是否高于 T 日最高价 | +| `next_high_profit_pct` | `number \| null` | T+1 最高价相对 T 日最高价的最大获利百分比 | + +响应示例: + +```json +{ + "trade_start_date": "2020-01-01", + "trade_end_date": "2026-05-16", + "start_time": "09:31:00", + "end_time": "09:59:59", + "symbol": "000001", + "ts_code": "000001.SZ", + "name": "示例股份", + "market": "主板", + "industry": "银行", + "second_board_count": 1, + "profit_data_count": 1, + "total_next_high_profit_pct": 4.12, + "average_next_high_profit_pct": 4.12, + "list": [ + { + "trade_date": "2026-04-24", + "previous_trade_date": "2026-04-23", + "previous_two_trade_date": "2026-04-22", + "next_trade_date": "2026-04-27", + "symbol": "000001", + "ts_code": "000001.SZ", + "name": "示例股份", + "market": "主板", + "industry": "银行", + "previous_close": 10, + "limit_price": 11, + "t_high": 11, + "t_close": 11, + "t_pct_chg": 10, + "status": "limit", + "first_limit_time": "09:35:12", + "open_times": 0, + "next_high": 11.45, + "next_close": 11.2, + "next_intraday_profitable": true, + "next_high_profit_pct": 4.09 + } + ] +} +``` + +## 9. 错误响应 + +```json +{ + "error": "参数错误" +} +``` diff --git a/internal/api/second_board_profit_handler.go b/internal/api/second_board_profit_handler.go new file mode 100644 index 0000000..312c9dc --- /dev/null +++ b/internal/api/second_board_profit_handler.go @@ -0,0 +1,67 @@ +package api + +import ( + "errors" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "level_go/internal/model" +) + +// GetSecondBoardProfitSummary 返回 2020 年至今二板早盘股票的 T+1 最大获利汇总。 +func (h *LimitBoardHandler) GetSecondBoardProfitSummary(c *gin.Context) { + req, err := parseSecondBoardProfitRequest(c, false) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.Service.GetSecondBoardProfitSummary(req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// GetSecondBoardProfitDetail 返回单只股票二板早盘后的 T+1 最大获利详情。 +func (h *LimitBoardHandler) GetSecondBoardProfitDetail(c *gin.Context) { + req, err := parseSecondBoardProfitRequest(c, true) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.Service.GetSecondBoardProfitDetail(req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, resp) +} + +// parseSecondBoardProfitRequest 解析二板早盘获利综合和详情接口参数。 +func parseSecondBoardProfitRequest(c *gin.Context, requireCode bool) (model.SecondBoardProfitRequest, error) { + req := model.SecondBoardProfitRequest{ + TradeStartDate: firstNonEmptyQuery(c, "tradeStartDate", "trade_start_date", "start_date"), + TradeEndDate: firstNonEmptyQuery(c, "tradeEndDate", "trade_end_date", "end_date"), + TsCode: firstNonEmptyQuery(c, "tsCode", "ts_code"), + Symbol: firstNonEmptyQuery(c, "symbol", "code"), + } + if req.TradeStartDate == "" { + req.TradeStartDate = defaultSecondProfitStartDate + } + if req.TradeEndDate == "" { + req.TradeEndDate = time.Now().Format("2006-01-02") + } + if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil { + return req, err + } + if requireCode && req.TsCode == "" && req.Symbol == "" { + return req, errors.New("tsCode 或 symbol 参数不能为空") + } + return req, nil +} diff --git a/internal/model/limit_board.go b/internal/model/limit_board.go index d3afe49..3317bc2 100644 --- a/internal/model/limit_board.go +++ b/internal/model/limit_board.go @@ -21,6 +21,14 @@ type SecondCalRequest struct { TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` // 查询结束 T 日。 } +// SecondBoardProfitRequest 是二板获利综合和详情接口的日期范围请求。 +type SecondBoardProfitRequest struct { + TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"` // 查询起始 T 日,默认 2020-01-01。 + TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` // 查询结束 T 日,默认当天。 + TsCode string `form:"tsCode" json:"ts_code"` // 可选 TuShare 股票代码,详情接口必填。 + Symbol string `form:"symbol" json:"symbol"` // 可选市场股票代码,详情接口可替代 tsCode。 +} + // LimitBoardStock 表示单只涨停股在涨停概览中的展示数据。 type LimitBoardStock struct { Symbol string `json:"symbol"` // 市场展示用股票代码。 @@ -165,6 +173,51 @@ type SecondCalStock struct { NextHighProfitPct *float64 `json:"next_high_profit_pct"` // T+1 最高价相对 T 日最高价的收益率。 } +// SecondBoardProfitSummaryItem 表示单只股票二板后的次日最大获利汇总。 +type SecondBoardProfitSummaryItem struct { + Symbol string `json:"symbol"` // 股票代码。 + TsCode string `json:"ts_code"` // TuShare 股票代码。 + Name string `json:"name"` // 股票简称。 + Market string `json:"market"` // 所属板块。 + Industry string `json:"industry"` // 所属行业。 + SecondBoardCount int `json:"second_board_count"` // 09:31:00-09:59:59 期间命中的二板次数。 + LimitCount int `json:"limit_count"` // 二板封板次数。 + BreakCount int `json:"break_count"` // 二板炸板次数。 + ProfitDataCount int `json:"profit_data_count"` // 具备 T+1 最大获利百分比的次数。 + TotalNextHighProfitPct float64 `json:"total_next_high_profit_pct"` // T+1 最大获利百分比累计值。 + AverageNextHighProfitPct float64 `json:"average_next_high_profit_pct"` // T+1 最大获利百分比平均值。 + FirstTradeDate string `json:"first_trade_date"` // 首次命中交易日。 + LastTradeDate string `json:"last_trade_date"` // 最近命中交易日。 +} + +// SecondBoardProfitSummaryResponse 是二板获利综合接口响应。 +type SecondBoardProfitSummaryResponse struct { + TradeStartDate string `json:"trade_start_date"` // 查询起始 T 日。 + TradeEndDate string `json:"trade_end_date"` // 查询结束 T 日。 + StartTime string `json:"start_time"` // 二板首次涨停时间筛选起点。 + EndTime string `json:"end_time"` // 二板首次涨停时间筛选终点。 + Total int `json:"total"` // 返回的股票数量。 + List []SecondBoardProfitSummaryItem `json:"list"` // 按累计获利从高到低排序。 +} + +// SecondBoardProfitDetailResponse 是单只股票二板获利详情接口响应。 +type SecondBoardProfitDetailResponse struct { + TradeStartDate string `json:"trade_start_date"` // 查询起始 T 日。 + TradeEndDate string `json:"trade_end_date"` // 查询结束 T 日。 + StartTime string `json:"start_time"` // 二板首次涨停时间筛选起点。 + EndTime string `json:"end_time"` // 二板首次涨停时间筛选终点。 + Symbol string `json:"symbol"` // 股票代码。 + TsCode string `json:"ts_code"` // TuShare 股票代码。 + Name string `json:"name"` // 股票简称。 + Market string `json:"market"` // 所属板块。 + Industry string `json:"industry"` // 所属行业。 + SecondBoardCount int `json:"second_board_count"` // 命中二板次数。 + ProfitDataCount int `json:"profit_data_count"` // 具备 T+1 最大获利百分比的次数。 + TotalNextHighProfitPct float64 `json:"total_next_high_profit_pct"` // T+1 最大获利百分比累计值。 + AverageNextHighProfitPct float64 `json:"average_next_high_profit_pct"` // T+1 最大获利百分比平均值。 + List []SecondCalStock `json:"list"` // 每次二板获利详情。 +} + // LimitOpenCompareGroup 是开盘价低于前收接口的分组结果。 type LimitOpenCompareGroup struct { Key string `json:"key"` // 分组唯一标识。 diff --git a/internal/router/router.go b/internal/router/router.go index 1e51d25..0574c2a 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -39,6 +39,8 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups) limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups) limitBoard.GET("/second-board-profit", limitBoardApi.ListSecondBoardProfit) + limitBoard.GET("/second-board-profit-summary", limitBoardApi.GetSecondBoardProfitSummary) + limitBoard.GET("/second-board-profit-detail", limitBoardApi.GetSecondBoardProfitDetail) limitBoard.GET("/second-cal", limitBoardApi.GetSecondCal) limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance) } diff --git a/internal/service/second_board_profit_service.go b/internal/service/second_board_profit_service.go new file mode 100644 index 0000000..9a0db90 --- /dev/null +++ b/internal/service/second_board_profit_service.go @@ -0,0 +1,257 @@ +package service // 声明 service 包,保持与涨停板服务层一致。 + +import ( // 引入本文件需要的标准库和项目包。 + "strings" // 用于清理请求中的股票代码空格。 + + "level_go/internal/model" // 引入接口请求和响应模型。 +) + +const ( // 定义二板获利缓存接口使用的常量。 + secondBoardProfitStartTime = "09:31:00" // 固定的二板首次涨停时间筛选起点。 + secondBoardProfitEndTime = "09:59:59" // 固定的二板首次涨停时间筛选终点。 +) + +type secondBoardProfitCacheRange struct { // 表示明细缓存表当前覆盖的交易日期范围。 + StartDate string `gorm:"column:start_date"` // 缓存明细最早交易日,格式 YYYY-MM-DD。 + EndDate string `gorm:"column:end_date"` // 缓存明细最晚交易日,格式 YYYY-MM-DD。 +} + +type secondBoardProfitDetailCacheRow struct { // 表示从二板获利详情缓存表扫描出的单行数据。 + TradeDate string `gorm:"column:trade_date"` // T 日交易日。 + PreviousTradeDate string `gorm:"column:previous_trade_date"` // T-1 交易日。 + PreviousTwoTradeDate string `gorm:"column:previous_two_trade_date"` // T-2 交易日。 + NextTradeDate string `gorm:"column:next_trade_date"` // T+1 交易日。 + Symbol string `gorm:"column:symbol"` // 股票代码。 + TsCode string `gorm:"column:ts_code"` // TuShare 股票代码。 + Name string `gorm:"column:name"` // 股票简称。 + Market string `gorm:"column:market"` // 所属板块。 + Industry string `gorm:"column:industry"` // 所属行业。 + PreviousClose *float64 `gorm:"column:previous_close"` // T-1 收盘价。 + LimitPrice *float64 `gorm:"column:limit_price"` // T 日涨停价。 + THigh *float64 `gorm:"column:t_high"` // T 日最高价。 + TClose *float64 `gorm:"column:t_close"` // T 日收盘价。 + TPctChg *float64 `gorm:"column:t_pct_chg"` // T 日涨跌幅。 + Status string `gorm:"column:status"` // 二板状态,limit 表示封板,break 表示炸板。 + FirstLimitTime string `gorm:"column:first_limit_time"` // T 日二板首次涨停时间。 + OpenTimes *int `gorm:"column:open_times"` // T 日开板次数。 + NextHigh *float64 `gorm:"column:next_high"` // T+1 最高价。 + NextClose *float64 `gorm:"column:next_close"` // T+1 收盘价。 + NextIntradayProfitable *int `gorm:"column:next_intraday_profitable"` // T+1 盘中是否盈利,数据库中为 0/1/null。 + NextHighProfitPct *float64 `gorm:"column:next_high_profit_pct"` // T+1 最大获利百分比。 +} + +// GetSecondBoardProfitSummary 返回二板获利综合数据。 +func (s *LimitBoardService) GetSecondBoardProfitSummary(req model.SecondBoardProfitRequest) (model.SecondBoardProfitSummaryResponse, error) { // 定义二板获利综合服务方法。 + resp := model.SecondBoardProfitSummaryResponse{ // 初始化响应结构。 + TradeStartDate: req.TradeStartDate, // 回显查询起始交易日。 + TradeEndDate: req.TradeEndDate, // 回显查询结束交易日。 + StartTime: secondBoardProfitStartTime, // 回显固定时间筛选起点。 + EndTime: secondBoardProfitEndTime, // 回显固定时间筛选终点。 + List: []model.SecondBoardProfitSummaryItem{}, // 初始化空列表,避免 JSON 返回 null。 + } + + useSummaryCache, err := s.shouldUseSecondBoardProfitSummaryCache(req) // 判断当前日期范围是否可以直接使用汇总缓存表。 + if err != nil { // 判断缓存范围查询是否失败。 + return resp, err // 返回已初始化响应和数据库错误。 + } + if useSummaryCache { // 请求范围覆盖整张明细缓存表时,直接读取预聚合汇总缓存表。 + return s.loadSecondBoardProfitSummaryCache(resp) // 从 summary cache 加载结果并返回。 + } + return s.loadSecondBoardProfitSummaryFromDetail(req, resp) // 窄日期范围从 detail cache 动态聚合,保持日期筛选语义。 +} + +// GetSecondBoardProfitDetail 返回单只股票二板获利详情数据。 +func (s *LimitBoardService) GetSecondBoardProfitDetail(req model.SecondBoardProfitRequest) (model.SecondBoardProfitDetailResponse, error) { // 定义个股二板详情服务方法。 + resp := model.SecondBoardProfitDetailResponse{ // 初始化响应结构。 + TradeStartDate: req.TradeStartDate, // 回显查询起始交易日。 + TradeEndDate: req.TradeEndDate, // 回显查询结束交易日。 + StartTime: secondBoardProfitStartTime, // 回显固定时间筛选起点。 + EndTime: secondBoardProfitEndTime, // 回显固定时间筛选终点。 + Symbol: strings.TrimSpace(req.Symbol), // 回显请求中的股票代码。 + TsCode: strings.TrimSpace(req.TsCode), // 回显请求中的 TuShare 股票代码。 + List: []model.SecondCalStock{}, // 初始化空列表,避免 JSON 返回 null。 + } + + rows, err := s.loadSecondBoardProfitDetailRows(req) // 从详情缓存表读取个股明细行。 + if err != nil { // 判断数据库查询是否失败。 + return resp, err // 返回已初始化响应和数据库错误。 + } + + for _, row := range rows { // 遍历缓存明细行并构建响应。 + stock := buildSecondBoardProfitStockFromCache(row) // 将缓存行转换为原接口复用的股票明细结构。 + if resp.SecondBoardCount == 0 { // 第一条命中记录用于补齐个股基础信息。 + resp.Symbol = stock.Symbol // 设置响应股票代码。 + resp.TsCode = stock.TsCode // 设置响应 TuShare 股票代码。 + resp.Name = stock.Name // 设置响应股票简称。 + resp.Market = stock.Market // 设置响应所属板块。 + resp.Industry = stock.Industry // 设置响应所属行业。 + } + resp.SecondBoardCount++ // 累加命中的二板次数。 + if stock.NextHighProfitPct != nil { // 只统计具备 T+1 最大获利百分比的数据。 + resp.ProfitDataCount++ // 累加具备收益数据的次数。 + resp.TotalNextHighProfitPct = roundSecondCalPct(resp.TotalNextHighProfitPct + *stock.NextHighProfitPct) // 累加最大获利百分比。 + } + resp.List = append(resp.List, stock) // 追加单次二板详情。 + } + if resp.ProfitDataCount > 0 { // 判断是否存在可计算平均值的数据。 + resp.AverageNextHighProfitPct = roundSecondCalPct(resp.TotalNextHighProfitPct / float64(resp.ProfitDataCount)) // 计算平均最大获利百分比。 + } + + return resp, nil // 返回详情响应。 +} + +// shouldUseSecondBoardProfitSummaryCache 判断是否可以使用预聚合汇总缓存表。 +func (s *LimitBoardService) shouldUseSecondBoardProfitSummaryCache(req model.SecondBoardProfitRequest) (bool, error) { // 定义汇总缓存使用判断函数。 + cacheRange, err := s.loadSecondBoardProfitCacheRange() // 查询明细缓存表当前覆盖日期范围。 + if err != nil { // 判断缓存范围查询是否失败。 + return false, err // 查询失败时返回错误。 + } + if cacheRange.StartDate == "" || cacheRange.EndDate == "" { // 判断缓存表是否为空。 + return false, nil // 缓存为空时不能使用汇总缓存。 + } + return req.TradeStartDate <= cacheRange.StartDate && req.TradeEndDate >= cacheRange.EndDate, nil // 请求覆盖缓存全范围时使用汇总缓存。 +} + +// loadSecondBoardProfitCacheRange 查询二板获利详情缓存表的日期覆盖范围。 +func (s *LimitBoardService) loadSecondBoardProfitCacheRange() (secondBoardProfitCacheRange, error) { // 定义缓存日期范围查询函数。 + // 定义查询缓存范围 SQL。 + query := ` +SELECT + COALESCE(DATE_FORMAT(MIN(trade_date), '%Y-%m-%d'), '') AS start_date, + COALESCE(DATE_FORMAT(MAX(trade_date), '%Y-%m-%d'), '') AS end_date +FROM second_board_profit_detail_cache` // 从明细缓存表读取最小和最大交易日。 + var cacheRange secondBoardProfitCacheRange // 声明缓存范围结果变量。 + err := s.DB.Raw(query).Scan(&cacheRange).Error // 执行 SQL 并扫描结果。 + return cacheRange, err // 返回缓存范围和可能的错误。 +} + +// loadSecondBoardProfitSummaryCache 从预聚合汇总缓存表读取完整汇总数据。 +func (s *LimitBoardService) loadSecondBoardProfitSummaryCache(resp model.SecondBoardProfitSummaryResponse) (model.SecondBoardProfitSummaryResponse, error) { // 定义汇总缓存读取函数。 + // 定义汇总缓存查询 SQL。 + query := ` +SELECT + symbol, + ts_code, + name, + market, + industry, + second_board_count, + limit_count, + break_count, + profit_data_count, + total_next_high_profit_pct, + average_next_high_profit_pct, + COALESCE(DATE_FORMAT(first_trade_date, '%Y-%m-%d'), '') AS first_trade_date, + COALESCE(DATE_FORMAT(last_trade_date, '%Y-%m-%d'), '') AS last_trade_date +FROM second_board_profit_summary_cache +ORDER BY total_next_high_profit_pct DESC, profit_data_count DESC, ts_code ASC` // 按累计获利、数据次数和股票代码稳定排序。 + if err := s.DB.Raw(query).Scan(&resp.List).Error; err != nil { // 执行汇总缓存查询。 + return resp, err // 查询失败时返回错误。 + } + resp.Total = len(resp.List) // 设置返回股票数量。 + return resp, nil // 返回汇总响应。 +} + +// loadSecondBoardProfitSummaryFromDetail 从明细缓存表按日期范围动态聚合汇总数据。 +func (s *LimitBoardService) loadSecondBoardProfitSummaryFromDetail(req model.SecondBoardProfitRequest, resp model.SecondBoardProfitSummaryResponse) (model.SecondBoardProfitSummaryResponse, error) { // 定义明细聚合函数。 + // 定义日期范围聚合 SQL。 + query := ` +SELECT + MAX(symbol) AS symbol, + ts_code, + MAX(name) AS name, + MAX(market) AS market, + MAX(industry) AS industry, + COUNT(*) AS second_board_count, + SUM(CASE WHEN status = 'limit' THEN 1 ELSE 0 END) AS limit_count, + SUM(CASE WHEN status = 'break' THEN 1 ELSE 0 END) AS break_count, + COUNT(next_high_profit_pct) AS profit_data_count, + ROUND(SUM(COALESCE(next_high_profit_pct, 0)), 2) AS total_next_high_profit_pct, + COALESCE(ROUND(SUM(COALESCE(next_high_profit_pct, 0)) / NULLIF(COUNT(next_high_profit_pct), 0), 2), 0) AS average_next_high_profit_pct, + COALESCE(DATE_FORMAT(MIN(trade_date), '%Y-%m-%d'), '') AS first_trade_date, + COALESCE(DATE_FORMAT(MAX(trade_date), '%Y-%m-%d'), '') AS last_trade_date +FROM second_board_profit_detail_cache +WHERE trade_date BETWEEN ? AND ? +GROUP BY ts_code +ORDER BY total_next_high_profit_pct DESC, profit_data_count DESC, ts_code ASC` // 在明细缓存表内按请求日期范围聚合。 + if err := s.DB.Raw(query, req.TradeStartDate, req.TradeEndDate).Scan(&resp.List).Error; err != nil { // 执行聚合查询。 + return resp, err // 查询失败时返回错误。 + } + resp.Total = len(resp.List) // 设置返回股票数量。 + return resp, nil // 返回汇总响应。 +} + +// loadSecondBoardProfitDetailRows 从明细缓存表读取单只股票详情。 +func (s *LimitBoardService) loadSecondBoardProfitDetailRows(req model.SecondBoardProfitRequest) ([]secondBoardProfitDetailCacheRow, error) { // 定义明细缓存查询函数。 + tsCode := strings.TrimSpace(req.TsCode) // 清理 TuShare 股票代码参数。 + symbol := strings.TrimSpace(req.Symbol) // 清理市场股票代码参数。 + // 定义明细缓存查询 SQL。 + query := ` +SELECT + DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date, + DATE_FORMAT(previous_trade_date, '%Y-%m-%d') AS previous_trade_date, + DATE_FORMAT(previous_two_trade_date, '%Y-%m-%d') AS previous_two_trade_date, + COALESCE(DATE_FORMAT(next_trade_date, '%Y-%m-%d'), '') AS next_trade_date, + symbol, + ts_code, + name, + market, + industry, + previous_close, + limit_price, + t_high, + t_close, + t_pct_chg, + status, + first_limit_time, + open_times, + next_high, + next_close, + next_intraday_profitable, + next_high_profit_pct +FROM second_board_profit_detail_cache +WHERE trade_date BETWEEN ? AND ?` // 先按日期范围过滤明细缓存。 + args := []interface{}{req.TradeStartDate, req.TradeEndDate} // 初始化 SQL 参数为日期范围。 + if tsCode != "" { // 优先按 TuShare 股票代码查询。 + query += " AND ts_code = ?" // 追加 TuShare 股票代码条件。 + args = append(args, tsCode) // 追加 TuShare 股票代码参数。 + } else { // 未传 TuShare 股票代码时使用市场股票代码。 + query += " AND symbol = ?" // 追加市场股票代码条件。 + args = append(args, symbol) // 追加市场股票代码参数。 + } + query += " ORDER BY trade_date ASC, ts_code ASC" // 追加稳定排序条件。 + var rows []secondBoardProfitDetailCacheRow // 声明明细缓存扫描结果。 + err := s.DB.Raw(query, args...).Scan(&rows).Error // 执行 SQL 并扫描结果。 + return rows, err // 返回明细行和可能的错误。 +} + +// buildSecondBoardProfitStockFromCache 将缓存行转换为接口复用的二板股票明细模型。 +func buildSecondBoardProfitStockFromCache(row secondBoardProfitDetailCacheRow) model.SecondCalStock { // 定义缓存行到响应模型的转换函数。 + item := model.SecondCalStock{ // 初始化二板股票明细。 + TradeDate: row.TradeDate, // 设置 T 日交易日。 + PreviousTradeDate: row.PreviousTradeDate, // 设置 T-1 交易日。 + PreviousTwoTradeDate: row.PreviousTwoTradeDate, // 设置 T-2 交易日。 + NextTradeDate: row.NextTradeDate, // 设置 T+1 交易日。 + Symbol: row.Symbol, // 设置股票代码。 + TsCode: row.TsCode, // 设置 TuShare 股票代码。 + Name: row.Name, // 设置股票简称。 + Market: row.Market, // 设置所属板块。 + Industry: row.Industry, // 设置所属行业。 + PreviousClose: row.PreviousClose, // 设置 T-1 收盘价。 + LimitPrice: row.LimitPrice, // 设置 T 日涨停价。 + THigh: row.THigh, // 设置 T 日最高价。 + TClose: row.TClose, // 设置 T 日收盘价。 + TPctChg: row.TPctChg, // 设置 T 日涨跌幅。 + Status: row.Status, // 设置二板状态。 + FirstLimitTime: row.FirstLimitTime, // 设置首次涨停时间。 + OpenTimes: row.OpenTimes, // 设置开板次数。 + NextHigh: row.NextHigh, // 设置 T+1 最高价。 + NextClose: row.NextClose, // 设置 T+1 收盘价。 + NextHighProfitPct: row.NextHighProfitPct, // 设置 T+1 最大获利百分比。 + } + if row.NextIntradayProfitable != nil { // 判断缓存中是否存在盈利布尔数据。 + nextIntradayProfitable := *row.NextIntradayProfitable != 0 // 将 0/1 转换为 bool。 + item.NextIntradayProfitable = &nextIntradayProfitable // 设置 T+1 盘中是否盈利。 + } + return item // 返回转换后的股票明细。 +}