Compare commits

...

2 Commits

Author SHA1 Message Date
yang a3292c3dd9 新增二板历史角度 2026-05-17 13:19:18 +08:00
yang 507b79f257 1、新增首页合计详情页;
2、新增索引和缓存;
2026-05-14 11:19:36 +08:00
12 changed files with 787 additions and 3 deletions
+9
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
"level_go/internal/api" "level_go/internal/api"
@@ -12,6 +13,8 @@ import (
// main 是服务启动入口,负责完成配置加载、数据库连接、依赖组装和 HTTP 路由启动。 // main 是服务启动入口,负责完成配置加载、数据库连接、依赖组装和 HTTP 路由启动。
func main() { func main() {
gin.SetMode(gin.ReleaseMode)
// 初始化配置文件,后续数据库 DSN 和服务端口都从全局配置中读取。 // 初始化配置文件,后续数据库 DSN 和服务端口都从全局配置中读取。
config.InitConfig() config.InitConfig()
@@ -21,6 +24,12 @@ func main() {
if err != nil { if err != nil {
log.Fatal("数据库连接失败:", err) log.Fatal("数据库连接失败:", err)
} }
sqlDB, err := db.DB()
if err != nil {
log.Fatal("获取数据库连接池失败:", err)
}
sqlDB.SetMaxIdleConns(20)
sqlDB.SetMaxOpenConns(100)
// 依赖注入:Service 持有数据库连接,Handler 持有对应 Service。 // 依赖注入:Service 持有数据库连接,Handler 持有对应 Service。
commonSrv := &service.CommonService{DB: db} commonSrv := &service.CommonService{DB: db}
+3
View File
@@ -0,0 +1,3 @@
ALTER TABLE `stock_limit_combined_data`
ADD INDEX `idx_ts_code_date_status` (`ts_code`, `trade_date`, `final_status`),
ADD INDEX `idx_trade_status_code` (`trade_date`, `final_status`, `ts_code`);
+3 -1
View File
@@ -110,5 +110,7 @@ CREATE TABLE `stock_limit_combined_data` (
UNIQUE KEY `uniq_date_code` (`trade_date`,`ts_code`), UNIQUE KEY `uniq_date_code` (`trade_date`,`ts_code`),
KEY `idx_trade_date` (`trade_date`), KEY `idx_trade_date` (`trade_date`),
KEY `idx_symbol` (`symbol`), KEY `idx_symbol` (`symbol`),
KEY `idx_trade_date_status` (`trade_date`,`final_status`) KEY `idx_trade_date_status` (`trade_date`,`final_status`),
KEY `idx_ts_code_date_status` (`ts_code`,`trade_date`,`final_status`),
KEY `idx_trade_status_code` (`trade_date`,`final_status`,`ts_code`)
) ENGINE=InnoDB AUTO_INCREMENT=167035 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=167035 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+3 -1
View File
@@ -4,7 +4,7 @@
- 服务前缀:`/api/v1` - 服务前缀:`/api/v1`
- 响应格式:`application/json` - 响应格式:`application/json`
- 当前项目接口总数:11 - 当前项目接口总数:13
## 2. 接口列表 ## 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-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/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` | 分页查询 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/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/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) | | 下载 | `GET` | `/api/v1/download/limit-board/second-board-profit-csv` | 下载筛选条件范围内的二板早盘次日盈利 CSV 明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) |
+230
View File
@@ -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": "参数错误"
}
```
+29
View File
@@ -63,6 +63,10 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
### 4.2 list[].stocks 明细字段 ### 4.2 list[].stocks 明细字段
`list[].stocks` 保留当日全部二板明细。
`list[].total_stocks` 为当日涨停和炸板合并后的明细列表,字段结构与 `stocks` 相同;该列表会剔除 `first_limit_time` 为空的数据,并按 `first_limit_time` 升序排序,时间相同时按 `ts_code` 升序排序。
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `trade_date` | `string` | T 日 | | `trade_date` | `string` | T 日 |
@@ -121,6 +125,31 @@ GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-0
"break_next_data_count": 3, "break_next_data_count": 3,
"break_next_profit_count": 3, "break_next_profit_count": 3,
"break_next_profit_rate": 100, "break_next_profit_rate": 100,
"total_stocks": [
{
"trade_date": "2026-05-07",
"previous_trade_date": "2026-05-06",
"previous_two_trade_date": "2026-04-30",
"next_trade_date": "2026-05-08",
"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.2,
"next_close": 11.1,
"next_intraday_profitable": true,
"next_high_profit_pct": 1.82
}
],
"stocks": [ "stocks": [
{ {
"trade_date": "2026-05-07", "trade_date": "2026-05-07",
@@ -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
}
+54
View File
@@ -21,6 +21,14 @@ type SecondCalRequest struct {
TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` // 查询结束 T 日。 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 表示单只涨停股在涨停概览中的展示数据。 // LimitBoardStock 表示单只涨停股在涨停概览中的展示数据。
type LimitBoardStock struct { type LimitBoardStock struct {
Symbol string `json:"symbol"` // 市场展示用股票代码。 Symbol string `json:"symbol"` // 市场展示用股票代码。
@@ -165,6 +173,51 @@ type SecondCalStock struct {
NextHighProfitPct *float64 `json:"next_high_profit_pct"` // T+1 最高价相对 T 日最高价的收益率。 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 是开盘价低于前收接口的分组结果。 // LimitOpenCompareGroup 是开盘价低于前收接口的分组结果。
type LimitOpenCompareGroup struct { type LimitOpenCompareGroup struct {
Key string `json:"key"` // 分组唯一标识。 Key string `json:"key"` // 分组唯一标识。
@@ -261,6 +314,7 @@ type SecondCalDay struct {
BreakNextProfitCount int `json:"break_next_profit_count"` // 当日炸板股 T+1 盘中盈利数量。 BreakNextProfitCount int `json:"break_next_profit_count"` // 当日炸板股 T+1 盘中盈利数量。
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // 当日炸板股 T+1 盘中盈利概率,百分比。 BreakNextProfitRate float64 `json:"break_next_profit_rate"` // 当日炸板股 T+1 盘中盈利概率,百分比。
Stocks []SecondCalStock `json:"stocks"` // 当日二板股票明细。 Stocks []SecondCalStock `json:"stocks"` // 当日二板股票明细。
TotalStocks []SecondCalStock `json:"total_stocks"` // 涨停和炸板合并后按首次涨停时间排序的明细。
} }
// SecondCalResponse 是二板日历接口响应。 // SecondCalResponse 是二板日历接口响应。
+115
View File
@@ -0,0 +1,115 @@
package router
import (
"bytes"
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
)
const (
responseCacheTTL = 5 * time.Minute
responseCacheMaxBody = 4 << 20
responseCacheMaxKeys = 512
)
type responseCacheEntry struct {
status int
contentType string
body []byte
expiresAt time.Time
}
type responseCacheStore struct {
mu sync.RWMutex
items map[string]responseCacheEntry
}
func newResponseCacheStore() *responseCacheStore {
return &responseCacheStore{items: make(map[string]responseCacheEntry)}
}
func (s *responseCacheStore) get(key string) (responseCacheEntry, bool) {
now := time.Now()
s.mu.RLock()
entry, ok := s.items[key]
s.mu.RUnlock()
if !ok {
return responseCacheEntry{}, false
}
if now.After(entry.expiresAt) {
s.mu.Lock()
if current, exists := s.items[key]; exists && now.After(current.expiresAt) {
delete(s.items, key)
}
s.mu.Unlock()
return responseCacheEntry{}, false
}
return entry, true
}
func (s *responseCacheStore) set(key string, entry responseCacheEntry) {
s.mu.Lock()
if len(s.items) >= responseCacheMaxKeys {
now := time.Now()
for cacheKey, current := range s.items {
if now.After(current.expiresAt) || len(s.items) >= responseCacheMaxKeys {
delete(s.items, cacheKey)
}
}
}
s.items[key] = entry
s.mu.Unlock()
}
type cachedResponseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w cachedResponseWriter) Write(data []byte) (int, error) {
w.body.Write(data)
return w.ResponseWriter.Write(data)
}
func cacheableGETResponseMiddleware(store *responseCacheStore) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method != http.MethodGet || strings.HasPrefix(c.Request.URL.Path, "/api/v1/download/") {
c.Next()
return
}
key := c.Request.URL.RequestURI()
if entry, ok := store.get(key); ok {
c.Header("Content-Type", entry.contentType)
c.Data(entry.status, entry.contentType, entry.body)
return
}
body := bytes.NewBuffer(nil)
writer := cachedResponseWriter{ResponseWriter: c.Writer, body: body}
c.Writer = writer
c.Next()
status := c.Writer.Status()
if status != http.StatusOK || body.Len() == 0 || body.Len() > responseCacheMaxBody {
return
}
contentType := c.Writer.Header().Get("Content-Type")
if !strings.Contains(contentType, "application/json") {
return
}
store.set(key, responseCacheEntry{
status: status,
contentType: contentType,
body: append([]byte(nil), body.Bytes()...),
expiresAt: time.Now().Add(responseCacheTTL),
})
}
}
+5 -1
View File
@@ -9,7 +9,8 @@ import (
// //
// 函数集中注册跨域中间件和 v1 版本接口,调用方只需要注入各业务 Handler。 // 函数集中注册跨域中间件和 v1 版本接口,调用方只需要注入各业务 Handler。
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine { func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine {
r := gin.Default() r := gin.New()
r.Use(gin.Recovery())
// 跨域中间件允许前端从任意来源访问接口,并暴露 CSV 下载需要的 Content-Disposition。 // 跨域中间件允许前端从任意来源访问接口,并暴露 CSV 下载需要的 Content-Disposition。
r.Use(func(c *gin.Context) { r.Use(func(c *gin.Context) {
@@ -23,6 +24,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
} }
c.Next() c.Next()
}) })
r.Use(cacheableGETResponseMiddleware(newResponseCacheStore()))
v1 := r.Group("/api/v1") v1 := r.Group("/api/v1")
{ {
@@ -37,6 +39,8 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups) limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups) limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
limitBoard.GET("/second-board-profit", limitBoardApi.ListSecondBoardProfit) 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("/second-cal", limitBoardApi.GetSecondCal)
limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance) limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
} }
@@ -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 // 返回转换后的股票明细。
}
+12
View File
@@ -2,6 +2,8 @@ package service
import ( import (
"math" "math"
"sort"
"strings"
"level_go/internal/model" "level_go/internal/model"
) )
@@ -57,6 +59,7 @@ func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.Seco
PreviousTwoTradeDate: day.PreviousTwoTradeDate, PreviousTwoTradeDate: day.PreviousTwoTradeDate,
NextTradeDate: day.NextTradeDate, NextTradeDate: day.NextTradeDate,
Stocks: []model.SecondCalStock{}, Stocks: []model.SecondCalStock{},
TotalStocks: []model.SecondCalStock{},
}) })
} }
@@ -298,10 +301,19 @@ func accumulateSecondCalDay(day *model.SecondCalDay, item model.SecondCalStock)
} }
} }
day.Stocks = append(day.Stocks, item) day.Stocks = append(day.Stocks, item)
if strings.TrimSpace(item.FirstLimitTime) != "" {
day.TotalStocks = append(day.TotalStocks, item)
}
} }
// finalizeSecondCalDay 计算单个交易日的概率字段。 // finalizeSecondCalDay 计算单个交易日的概率字段。
func finalizeSecondCalDay(day *model.SecondCalDay) { func finalizeSecondCalDay(day *model.SecondCalDay) {
sort.SliceStable(day.TotalStocks, func(i, j int) bool {
if day.TotalStocks[i].FirstLimitTime == day.TotalStocks[j].FirstLimitTime {
return day.TotalStocks[i].TsCode < day.TotalStocks[j].TsCode
}
return day.TotalStocks[i].FirstLimitTime < day.TotalStocks[j].FirstLimitTime
})
day.NextIntradayProfitRate = secondCalRate(day.NextIntradayProfitCount, day.NextTradeDataCount) day.NextIntradayProfitRate = secondCalRate(day.NextIntradayProfitCount, day.NextTradeDataCount)
day.LimitNextProfitRate = secondCalRate(day.LimitNextProfitCount, day.LimitNextDataCount) day.LimitNextProfitRate = secondCalRate(day.LimitNextProfitCount, day.LimitNextDataCount)
day.BreakNextProfitRate = secondCalRate(day.BreakNextProfitCount, day.BreakNextDataCount) day.BreakNextProfitRate = secondCalRate(day.BreakNextProfitCount, day.BreakNextDataCount)