Compare commits
2 Commits
8504838ccb
...
faf535d889
| Author | SHA1 | Date | |
|---|---|---|---|
| faf535d889 | |||
| d4bb849f79 |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- 服务前缀:`/api/v1`
|
- 服务前缀:`/api/v1`
|
||||||
- 响应格式:`application/json`
|
- 响应格式:`application/json`
|
||||||
- 当前项目接口总数:10
|
- 当前项目接口总数:11
|
||||||
|
|
||||||
## 2. 接口列表
|
## 2. 接口列表
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
| 涨停低开分组 | `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-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) |
|
||||||
| 下载 | `GET` | `/api/v1/download/limit-board/break-next-performance-csv` | 下载筛选条件范围内的炸板次日表现 CSV 明细 | [limit-board-break-next-performance.md](./limit-board-break-next-performance.md) |
|
| 下载 | `GET` | `/api/v1/download/limit-board/break-next-performance-csv` | 下载筛选条件范围内的炸板次日表现 CSV 明细 | [limit-board-break-next-performance.md](./limit-board-break-next-performance.md) |
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# 二板日历接口文档
|
||||||
|
|
||||||
|
## 1. 基本信息
|
||||||
|
|
||||||
|
- 接口名称:二板日历
|
||||||
|
- 请求方法:`GET`
|
||||||
|
- 请求路径:`/api/v1/limit-board/second-cal`
|
||||||
|
- 接口用途:按交易日统计每日二板数量、二板封板数量、二板炸板数量,以及二板股票 T+1 盘中盈利概率。
|
||||||
|
|
||||||
|
## 2. 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `tradeStartDate` | `string` | 是 | 无 | T 日起始日期,格式 `YYYY-MM-DD` |
|
||||||
|
| `tradeEndDate` | `string` | 是 | 无 | T 日结束日期,格式 `YYYY-MM-DD` |
|
||||||
|
|
||||||
|
兼容 `start_date`、`end_date` 作为日期参数别名。
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/limit-board/second-cal?tradeStartDate=2026-05-07&tradeEndDate=2026-05-11
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 统计规则
|
||||||
|
|
||||||
|
- 以 `trade_calendar` 中 `exchange = 'SSE'` 且 `is_open = 1` 的记录推导 T-2、T-1、T、T+1 交易日。
|
||||||
|
- T-1 涨停样本来自 `limit_list_d`,条件为 `limit = 'U'`。
|
||||||
|
- T-2 同一股票不能在 `limit_list_d` 中存在 `limit = 'U'` 的记录。
|
||||||
|
- 股票所属板块和行业来自 `stock_basic.market`、`stock_basic.industry`。
|
||||||
|
- T 日行情来自 `stock_daily`,当 T 日最高价 `high` 触及按 T-1 收盘价和板块涨跌幅规则计算的涨停价时,计为二板。
|
||||||
|
- 涨停价比例按板块计算:北交所 30%,创业板/科创板 20%,其余名称包含 ST 的股票 5%,其他股票 10%。
|
||||||
|
- T 日 `close` 与 `high` 一致时,计为二板封板,`status = "limit"`。
|
||||||
|
- T 日 `close` 小于 `high` 时,计为二板炸板,`status = "break"`。
|
||||||
|
- T+1 盘中盈利统一按 `T+1 high > T high` 判断,概率分母只统计具备 T 日最高价和 T+1 最高价的数据。
|
||||||
|
|
||||||
|
## 4. 响应字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `trade_start_date` | `string` | 查询起始 T 日 |
|
||||||
|
| `trade_end_date` | `string` | 查询结束 T 日 |
|
||||||
|
| `summary` | `object` | 日期范围汇总统计 |
|
||||||
|
| `list` | `array` | 每个交易日的二板日历统计 |
|
||||||
|
|
||||||
|
### 4.1 summary/list 单日统计字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `second_board_count` | `int` | 二板股票数量 |
|
||||||
|
| `limit_count` | `int` | 二板封板数量 |
|
||||||
|
| `break_count` | `int` | 二板炸板数量 |
|
||||||
|
| `next_trade_data_count` | `int` | 具备 T+1 盈利判断数据的数量 |
|
||||||
|
| `next_intraday_profit_count` | `int` | T+1 盘中盈利数量 |
|
||||||
|
| `next_intraday_profit_rate` | `number` | T+1 盘中盈利概率,百分比 |
|
||||||
|
| `limit_next_data_count` | `int` | 封板股中具备 T+1 盈利判断数据的数量 |
|
||||||
|
| `limit_next_profit_count` | `int` | 封板股 T+1 盘中盈利数量 |
|
||||||
|
| `limit_next_profit_rate` | `number` | 封板股 T+1 盘中盈利概率,百分比 |
|
||||||
|
| `break_next_data_count` | `int` | 炸板股中具备 T+1 盈利判断数据的数量 |
|
||||||
|
| `break_next_profit_count` | `int` | 炸板股 T+1 盘中盈利数量 |
|
||||||
|
| `break_next_profit_rate` | `number` | 炸板股 T+1 盘中盈利概率,百分比 |
|
||||||
|
|
||||||
|
### 4.2 list[].stocks 明细字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `trade_date` | `string` | T 日 |
|
||||||
|
| `previous_trade_date` | `string` | T-1 交易日 |
|
||||||
|
| `previous_two_trade_date` | `string` | T-2 交易日 |
|
||||||
|
| `next_trade_date` | `string` | T+1 交易日 |
|
||||||
|
| `symbol` / `ts_code` / `name` | `string` | 股票标识 |
|
||||||
|
| `market` / `industry` | `string` | 板块和行业 |
|
||||||
|
| `previous_close` | `number \| null` | T-1 收盘价 |
|
||||||
|
| `limit_price` | `number \| null` | T 日涨停价 |
|
||||||
|
| `t_high` / `t_close` | `number \| null` | T 日最高价和收盘价 |
|
||||||
|
| `t_pct_chg` | `number \| null` | T 日收盘涨跌幅 |
|
||||||
|
| `status` | `string` | `limit` 表示封板,`break` 表示炸板 |
|
||||||
|
| `next_high` / `next_close` | `number \| null` | T+1 最高价和收盘价 |
|
||||||
|
| `next_intraday_profitable` | `boolean \| null` | T+1 盘中是否盈利 |
|
||||||
|
| `next_high_profit_pct` | `number \| null` | T+1 最高价相对 T 日最高价的收益率 |
|
||||||
|
|
||||||
|
## 5. 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"trade_start_date": "2026-05-07",
|
||||||
|
"trade_end_date": "2026-05-11",
|
||||||
|
"summary": {
|
||||||
|
"trade_day_count": 3,
|
||||||
|
"second_board_count": 51,
|
||||||
|
"limit_count": 41,
|
||||||
|
"break_count": 10,
|
||||||
|
"next_trade_data_count": 35,
|
||||||
|
"next_intraday_profit_count": 34,
|
||||||
|
"next_intraday_profit_rate": 97.14,
|
||||||
|
"limit_next_data_count": 29,
|
||||||
|
"limit_next_profit_count": 28,
|
||||||
|
"limit_next_profit_rate": 96.55,
|
||||||
|
"break_next_data_count": 6,
|
||||||
|
"break_next_profit_count": 6,
|
||||||
|
"break_next_profit_rate": 100
|
||||||
|
},
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"trade_date": "2026-05-07",
|
||||||
|
"previous_trade_date": "2026-05-06",
|
||||||
|
"previous_two_trade_date": "2026-04-30",
|
||||||
|
"next_trade_date": "2026-05-08",
|
||||||
|
"second_board_count": 19,
|
||||||
|
"limit_count": 16,
|
||||||
|
"break_count": 3,
|
||||||
|
"next_trade_data_count": 19,
|
||||||
|
"next_intraday_profit_count": 18,
|
||||||
|
"next_intraday_profit_rate": 94.74,
|
||||||
|
"limit_next_data_count": 16,
|
||||||
|
"limit_next_profit_count": 15,
|
||||||
|
"limit_next_profit_rate": 93.75,
|
||||||
|
"break_next_data_count": 3,
|
||||||
|
"break_next_profit_count": 3,
|
||||||
|
"break_next_profit_rate": 100,
|
||||||
|
"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",
|
||||||
|
"next_high": 11.2,
|
||||||
|
"next_close": 11.1,
|
||||||
|
"next_intraday_profitable": true,
|
||||||
|
"next_high_profit_pct": 1.82
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -72,6 +72,41 @@ func (h *LimitBoardHandler) GetBreakNextPerformance(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, resp)
|
c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSecondCal 返回日期范围内的二板日历统计。
|
||||||
|
func (h *LimitBoardHandler) GetSecondCal(c *gin.Context) {
|
||||||
|
req, err := parseSecondCalRequest(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.Service.GetSecondCal(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSecondCalRequest 解析二板日历接口的日期范围参数。
|
||||||
|
func parseSecondCalRequest(c *gin.Context) (model.SecondCalRequest, error) {
|
||||||
|
req := model.SecondCalRequest{
|
||||||
|
TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))),
|
||||||
|
TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))),
|
||||||
|
}
|
||||||
|
if req.TradeStartDate == "" {
|
||||||
|
return req, errors.New("tradeStartDate 参数不能为空")
|
||||||
|
}
|
||||||
|
if req.TradeEndDate == "" {
|
||||||
|
return req, errors.New("tradeEndDate 参数不能为空")
|
||||||
|
}
|
||||||
|
if err := validateSecondProfitDateRange(req.TradeStartDate, req.TradeEndDate); err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseLimitBreakNextPerformanceRequest 解析炸板次日表现接口的查询参数。
|
// parseLimitBreakNextPerformanceRequest 解析炸板次日表现接口的查询参数。
|
||||||
//
|
//
|
||||||
// includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。
|
// includePagination 为 false 时跳过分页参数,供 CSV 导出复用同一套日期和过滤条件解析逻辑。
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ type LimitBreakNextPerformanceRequest struct {
|
|||||||
BreakTimeBefore1100 *bool `json:"break_time_before_1100,omitempty"` // 可选过滤:炸板时间是否早于 11:00。
|
BreakTimeBefore1100 *bool `json:"break_time_before_1100,omitempty"` // 可选过滤:炸板时间是否早于 11:00。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecondCalRequest 是二板日历接口的日期范围请求。
|
||||||
|
type SecondCalRequest struct {
|
||||||
|
TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"` // 查询起始 T 日。
|
||||||
|
TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"` // 查询结束 T 日。
|
||||||
|
}
|
||||||
|
|
||||||
// LimitBoardStock 表示单只涨停股在涨停概览中的展示数据。
|
// LimitBoardStock 表示单只涨停股在涨停概览中的展示数据。
|
||||||
type LimitBoardStock struct {
|
type LimitBoardStock struct {
|
||||||
Symbol string `json:"symbol"` // 市场展示用股票代码。
|
Symbol string `json:"symbol"` // 市场展示用股票代码。
|
||||||
@@ -134,6 +140,29 @@ type LimitBreakNextPerformanceStock struct {
|
|||||||
NextClosePctChg *float64 `json:"next_close_pct_chg"` // T+1 收盘相对 T 日收盘的收益率。
|
NextClosePctChg *float64 `json:"next_close_pct_chg"` // T+1 收盘相对 T 日收盘的收益率。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecondCalStock 表示二板日历中单只二板股票的 T 日和 T+1 表现。
|
||||||
|
type SecondCalStock struct {
|
||||||
|
TradeDate string `json:"trade_date"` // T 日交易日。
|
||||||
|
PreviousTradeDate string `json:"previous_trade_date"` // T-1 交易日。
|
||||||
|
PreviousTwoTradeDate string `json:"previous_two_trade_date"` // T-2 交易日。
|
||||||
|
NextTradeDate string `json:"next_trade_date"` // T+1 交易日。
|
||||||
|
Symbol string `json:"symbol"` // 股票代码。
|
||||||
|
TsCode string `json:"ts_code"` // TuShare 股票代码。
|
||||||
|
Name string `json:"name"` // 股票简称。
|
||||||
|
Market string `json:"market"` // 所属板块,来自 stock_basic.market。
|
||||||
|
Industry string `json:"industry"` // 所属行业,来自 stock_basic.industry。
|
||||||
|
PreviousClose *float64 `json:"previous_close"` // T-1 收盘价。
|
||||||
|
LimitPrice *float64 `json:"limit_price"` // 按 T-1 收盘价和板块涨跌幅规则计算的 T 日涨停价。
|
||||||
|
THigh *float64 `json:"t_high"` // T 日最高价。
|
||||||
|
TClose *float64 `json:"t_close"` // T 日收盘价。
|
||||||
|
TPctChg *float64 `json:"t_pct_chg"` // T 日收盘涨跌幅。
|
||||||
|
Status string `json:"status"` // T 日二板状态:limit 表示封板,break 表示炸板。
|
||||||
|
NextHigh *float64 `json:"next_high"` // T+1 日最高价。
|
||||||
|
NextClose *float64 `json:"next_close"` // T+1 日收盘价。
|
||||||
|
NextIntradayProfitable *bool `json:"next_intraday_profitable"` // T+1 盘中最高价是否高于 T 日最高价。
|
||||||
|
NextHighProfitPct *float64 `json:"next_high_profit_pct"` // T+1 最高价相对 T 日最高价的收益率。
|
||||||
|
}
|
||||||
|
|
||||||
// LimitOpenCompareGroup 是开盘价低于前收接口的分组结果。
|
// LimitOpenCompareGroup 是开盘价低于前收接口的分组结果。
|
||||||
type LimitOpenCompareGroup struct {
|
type LimitOpenCompareGroup struct {
|
||||||
Key string `json:"key"` // 分组唯一标识。
|
Key string `json:"key"` // 分组唯一标识。
|
||||||
@@ -173,6 +202,23 @@ type LimitBreakNextPerformanceSummary struct {
|
|||||||
NextTradeDataCount int `json:"next_trade_data_count"` // 具备 T+1 行情数据的股票数量。
|
NextTradeDataCount int `json:"next_trade_data_count"` // 具备 T+1 行情数据的股票数量。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecondCalSummary 汇总日期范围内所有二板日历指标。
|
||||||
|
type SecondCalSummary struct {
|
||||||
|
TradeDayCount int `json:"trade_day_count"` // 返回的交易日数量。
|
||||||
|
SecondBoardCount int `json:"second_board_count"` // 触及二板涨停价的股票总数。
|
||||||
|
LimitCount int `json:"limit_count"` // 二板中封住涨停的股票数量。
|
||||||
|
BreakCount int `json:"break_count"` // 二板中炸板的股票数量。
|
||||||
|
NextTradeDataCount int `json:"next_trade_data_count"` // 具备 T+1 最高价和 T 日最高价的股票数量。
|
||||||
|
NextIntradayProfitCount int `json:"next_intraday_profit_count"` // T+1 最高价高于 T 日最高价的股票数量。
|
||||||
|
NextIntradayProfitRate float64 `json:"next_intraday_profit_rate"` // T+1 盘中盈利概率,百分比。
|
||||||
|
LimitNextDataCount int `json:"limit_next_data_count"` // T 日封板股票中具备 T+1 盈利判断数据的数量。
|
||||||
|
LimitNextProfitCount int `json:"limit_next_profit_count"` // T 日封板股票中 T+1 盘中盈利数量。
|
||||||
|
LimitNextProfitRate float64 `json:"limit_next_profit_rate"` // T 日封板股票的 T+1 盘中盈利概率,百分比。
|
||||||
|
BreakNextDataCount int `json:"break_next_data_count"` // T 日炸板股票中具备 T+1 盈利判断数据的数量。
|
||||||
|
BreakNextProfitCount int `json:"break_next_profit_count"` // T 日炸板股票中 T+1 盘中盈利数量。
|
||||||
|
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // T 日炸板股票的 T+1 盘中盈利概率,百分比。
|
||||||
|
}
|
||||||
|
|
||||||
// LimitOpenCompareResponse 是开盘价低于前收接口响应。
|
// LimitOpenCompareResponse 是开盘价低于前收接口响应。
|
||||||
type LimitOpenCompareResponse struct {
|
type LimitOpenCompareResponse struct {
|
||||||
TradeDate string `json:"trade_date"` // 查询交易日。
|
TradeDate string `json:"trade_date"` // 查询交易日。
|
||||||
@@ -194,6 +240,35 @@ type LimitBreakNextPerformanceResponse struct {
|
|||||||
List []LimitBreakNextPerformanceStock `json:"list"` // 当前页记录。
|
List []LimitBreakNextPerformanceStock `json:"list"` // 当前页记录。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecondCalDay 表示二板日历中单个交易日的聚合统计。
|
||||||
|
type SecondCalDay struct {
|
||||||
|
TradeDate string `json:"trade_date"` // T 日交易日。
|
||||||
|
PreviousTradeDate string `json:"previous_trade_date"` // T-1 交易日。
|
||||||
|
PreviousTwoTradeDate string `json:"previous_two_trade_date"` // T-2 交易日。
|
||||||
|
NextTradeDate string `json:"next_trade_date"` // T+1 交易日。
|
||||||
|
SecondBoardCount int `json:"second_board_count"` // 当日触及二板涨停价的股票数量。
|
||||||
|
LimitCount int `json:"limit_count"` // 当日二板封板数量。
|
||||||
|
BreakCount int `json:"break_count"` // 当日二板炸板数量。
|
||||||
|
NextTradeDataCount int `json:"next_trade_data_count"` // 当日具备 T+1 盈利判断数据的股票数量。
|
||||||
|
NextIntradayProfitCount int `json:"next_intraday_profit_count"` // 当日 T+1 最高价高于 T 日最高价数量。
|
||||||
|
NextIntradayProfitRate float64 `json:"next_intraday_profit_rate"` // 当日 T+1 盘中盈利概率,百分比。
|
||||||
|
LimitNextDataCount int `json:"limit_next_data_count"` // 当日封板股中具备 T+1 盈利判断数据的数量。
|
||||||
|
LimitNextProfitCount int `json:"limit_next_profit_count"` // 当日封板股 T+1 盘中盈利数量。
|
||||||
|
LimitNextProfitRate float64 `json:"limit_next_profit_rate"` // 当日封板股 T+1 盘中盈利概率,百分比。
|
||||||
|
BreakNextDataCount int `json:"break_next_data_count"` // 当日炸板股中具备 T+1 盈利判断数据的数量。
|
||||||
|
BreakNextProfitCount int `json:"break_next_profit_count"` // 当日炸板股 T+1 盘中盈利数量。
|
||||||
|
BreakNextProfitRate float64 `json:"break_next_profit_rate"` // 当日炸板股 T+1 盘中盈利概率,百分比。
|
||||||
|
Stocks []SecondCalStock `json:"stocks"` // 当日二板股票明细。
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecondCalResponse 是二板日历接口响应。
|
||||||
|
type SecondCalResponse struct {
|
||||||
|
TradeStartDate string `json:"trade_start_date"` // 查询起始 T 日。
|
||||||
|
TradeEndDate string `json:"trade_end_date"` // 查询结束 T 日。
|
||||||
|
Summary SecondCalSummary `json:"summary"` // 日期范围汇总统计。
|
||||||
|
List []SecondCalDay `json:"list"` // 按交易日升序排列的日历统计。
|
||||||
|
}
|
||||||
|
|
||||||
// LimitOpenThresholdResponse 是开盘价阈值分组接口响应。
|
// LimitOpenThresholdResponse 是开盘价阈值分组接口响应。
|
||||||
type LimitOpenThresholdResponse struct {
|
type LimitOpenThresholdResponse struct {
|
||||||
TradeDate string `json:"trade_date"` // 查询交易日。
|
TradeDate string `json:"trade_date"` // 查询交易日。
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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-cal", limitBoardApi.GetSecondCal)
|
||||||
limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
|
limitBoard.GET("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"level_go/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// secondCalRow 是二板日历 SQL 查询扫描出的单股基础数据。
|
||||||
|
type secondCalRow 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"` // T 日二板状态。
|
||||||
|
NextHigh *float64 `gorm:"column:next_high"` // T+1 日最高价。
|
||||||
|
NextClose *float64 `gorm:"column:next_close"` // T+1 日收盘价。
|
||||||
|
}
|
||||||
|
|
||||||
|
// secondCalDaySeed 是日期范围内每个交易日的前后交易日信息。
|
||||||
|
type secondCalDaySeed 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 交易日。
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSecondCal 返回按交易日聚合的二板日历统计。
|
||||||
|
func (s *LimitBoardService) GetSecondCal(req model.SecondCalRequest) (model.SecondCalResponse, error) {
|
||||||
|
resp := model.SecondCalResponse{
|
||||||
|
TradeStartDate: req.TradeStartDate,
|
||||||
|
TradeEndDate: req.TradeEndDate,
|
||||||
|
List: []model.SecondCalDay{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先加载完整交易日骨架,确保没有二板的交易日也能出现在日历中。
|
||||||
|
days, dayIndex, err := s.loadSecondCalDays(req.TradeStartDate, req.TradeEndDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
for _, day := range days {
|
||||||
|
resp.List = append(resp.List, model.SecondCalDay{
|
||||||
|
TradeDate: day.TradeDate,
|
||||||
|
PreviousTradeDate: day.PreviousTradeDate,
|
||||||
|
PreviousTwoTradeDate: day.PreviousTwoTradeDate,
|
||||||
|
NextTradeDate: day.NextTradeDate,
|
||||||
|
Stocks: []model.SecondCalStock{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再加载命中二板口径的单股明细,并累加到对应交易日。
|
||||||
|
rows, err := s.loadSecondCalRows(req.TradeStartDate, req.TradeEndDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
item := buildSecondCalStock(row)
|
||||||
|
idx, ok := dayIndex[item.TradeDate]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
accumulateSecondCalDay(&resp.List[idx], item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后汇总整个日期范围,并补齐各交易日的概率字段。
|
||||||
|
resp.Summary.TradeDayCount = len(resp.List)
|
||||||
|
for i := range resp.List {
|
||||||
|
finalizeSecondCalDay(&resp.List[i])
|
||||||
|
accumulateSecondCalSummary(&resp.Summary, resp.List[i])
|
||||||
|
}
|
||||||
|
finalizeSecondCalSummary(&resp.Summary)
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadSecondCalDays 查询日期范围内的开市交易日和相邻交易日。
|
||||||
|
func (s *LimitBoardService) loadSecondCalDays(startDate, endDate string) ([]secondCalDaySeed, map[string]int, error) {
|
||||||
|
query := `
|
||||||
|
WITH open_calendar AS (
|
||||||
|
SELECT
|
||||||
|
cal_date AS trade_date,
|
||||||
|
LAG(cal_date, 1) OVER (ORDER BY cal_date) AS previous_trade_date,
|
||||||
|
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
|
||||||
|
LEAD(cal_date, 1) OVER (ORDER BY cal_date) AS next_trade_date
|
||||||
|
FROM trade_calendar
|
||||||
|
WHERE exchange = 'SSE' AND is_open = 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date,
|
||||||
|
COALESCE(DATE_FORMAT(previous_trade_date, '%Y-%m-%d'), '') AS previous_trade_date,
|
||||||
|
COALESCE(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
|
||||||
|
FROM open_calendar
|
||||||
|
WHERE trade_date BETWEEN ? AND ?
|
||||||
|
ORDER BY trade_date ASC`
|
||||||
|
|
||||||
|
var days []secondCalDaySeed
|
||||||
|
if err := s.DB.Raw(query, startDate, endDate).Scan(&days).Error; err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建交易日到响应数组下标的映射,后续聚合明细时可以 O(1) 定位。
|
||||||
|
dayIndex := make(map[string]int, len(days))
|
||||||
|
for i, day := range days {
|
||||||
|
dayIndex[day.TradeDate] = i
|
||||||
|
}
|
||||||
|
return days, dayIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadSecondCalRows 查询 T-1 涨停、T-2 未涨停、T 日再次触及涨停价的二板股票。
|
||||||
|
func (s *LimitBoardService) loadSecondCalRows(startDate, endDate string) ([]secondCalRow, error) {
|
||||||
|
query := `
|
||||||
|
WITH open_calendar AS (
|
||||||
|
SELECT
|
||||||
|
cal_date AS trade_date,
|
||||||
|
LAG(cal_date, 1) OVER (ORDER BY cal_date) AS previous_trade_date,
|
||||||
|
LAG(cal_date, 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
|
||||||
|
LEAD(cal_date, 1) OVER (ORDER BY cal_date) AS next_trade_date
|
||||||
|
FROM trade_calendar
|
||||||
|
WHERE exchange = 'SSE' AND is_open = 1
|
||||||
|
),
|
||||||
|
candidate AS (
|
||||||
|
SELECT
|
||||||
|
c.trade_date,
|
||||||
|
c.previous_trade_date,
|
||||||
|
c.previous_two_trade_date,
|
||||||
|
c.next_trade_date,
|
||||||
|
d.symbol,
|
||||||
|
pl.ts_code,
|
||||||
|
COALESCE(NULLIF(sb.name, ''), NULLIF(pl.name, ''), '') AS name,
|
||||||
|
COALESCE(NULLIF(sb.market, ''), '') AS market,
|
||||||
|
COALESCE(NULLIF(sb.industry, ''), NULLIF(pl.industry, ''), '') AS industry,
|
||||||
|
COALESCE(d.pre_close, pl.close) AS previous_close,
|
||||||
|
ROUND(
|
||||||
|
COALESCE(d.pre_close, pl.close) *
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(sb.market, '') = '北交所' THEN 1.30
|
||||||
|
WHEN COALESCE(sb.market, '') IN ('创业板', '科创板') THEN 1.20
|
||||||
|
WHEN COALESCE(sb.name, pl.name, '') LIKE '%ST%' THEN 1.05
|
||||||
|
ELSE 1.10
|
||||||
|
END,
|
||||||
|
2
|
||||||
|
) AS limit_price,
|
||||||
|
d.high AS t_high,
|
||||||
|
d.close AS t_close,
|
||||||
|
d.pct_chg AS t_pct_chg,
|
||||||
|
CASE
|
||||||
|
WHEN ABS(d.close - d.high) < 0.0001 THEN 'limit'
|
||||||
|
WHEN d.close < d.high THEN 'break'
|
||||||
|
ELSE 'unknown'
|
||||||
|
END AS status,
|
||||||
|
nd.high AS next_high,
|
||||||
|
nd.close AS next_close
|
||||||
|
FROM open_calendar c
|
||||||
|
INNER JOIN limit_list_d pl
|
||||||
|
ON pl.trade_date = c.previous_trade_date AND pl.` + "`limit`" + ` = 'U'
|
||||||
|
LEFT JOIN limit_list_d ppl
|
||||||
|
ON ppl.ts_code = pl.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.` + "`limit`" + ` = 'U'
|
||||||
|
INNER JOIN stock_daily d
|
||||||
|
ON d.ts_code = pl.ts_code AND d.trade_date = c.trade_date
|
||||||
|
LEFT JOIN stock_daily nd
|
||||||
|
ON nd.ts_code = pl.ts_code AND nd.trade_date = c.next_trade_date
|
||||||
|
LEFT JOIN stock_basic sb
|
||||||
|
ON sb.ts_code = pl.ts_code COLLATE utf8mb4_unicode_ci
|
||||||
|
WHERE c.trade_date BETWEEN ? AND ?
|
||||||
|
AND c.previous_trade_date IS NOT NULL
|
||||||
|
AND c.previous_two_trade_date IS NOT NULL
|
||||||
|
AND ppl.ts_code IS NULL
|
||||||
|
AND d.high IS NOT NULL
|
||||||
|
AND d.close IS NOT NULL
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
COALESCE(symbol, '') AS symbol,
|
||||||
|
ts_code,
|
||||||
|
name,
|
||||||
|
market,
|
||||||
|
industry,
|
||||||
|
previous_close,
|
||||||
|
limit_price,
|
||||||
|
t_high,
|
||||||
|
t_close,
|
||||||
|
t_pct_chg,
|
||||||
|
status,
|
||||||
|
next_high,
|
||||||
|
next_close
|
||||||
|
FROM candidate
|
||||||
|
WHERE previous_close IS NOT NULL
|
||||||
|
AND limit_price IS NOT NULL
|
||||||
|
AND limit_price > 0
|
||||||
|
AND t_high >= limit_price - 0.0001
|
||||||
|
AND status IN ('limit', 'break')
|
||||||
|
ORDER BY trade_date ASC, ts_code ASC`
|
||||||
|
|
||||||
|
var rows []secondCalRow
|
||||||
|
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSecondCalStock 把数据库行转换成接口明细,并计算 T+1 盘中收益。
|
||||||
|
func buildSecondCalStock(row secondCalRow) model.SecondCalStock {
|
||||||
|
item := model.SecondCalStock{
|
||||||
|
TradeDate: row.TradeDate,
|
||||||
|
PreviousTradeDate: row.PreviousTradeDate,
|
||||||
|
PreviousTwoTradeDate: row.PreviousTwoTradeDate,
|
||||||
|
NextTradeDate: row.NextTradeDate,
|
||||||
|
Symbol: row.Symbol,
|
||||||
|
TsCode: row.TsCode,
|
||||||
|
Name: row.Name,
|
||||||
|
Market: row.Market,
|
||||||
|
Industry: row.Industry,
|
||||||
|
PreviousClose: row.PreviousClose,
|
||||||
|
LimitPrice: row.LimitPrice,
|
||||||
|
THigh: row.THigh,
|
||||||
|
TClose: row.TClose,
|
||||||
|
TPctChg: row.TPctChg,
|
||||||
|
Status: row.Status,
|
||||||
|
NextHigh: row.NextHigh,
|
||||||
|
NextClose: row.NextClose,
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.THigh != nil && *row.THigh > 0 && row.NextHigh != nil {
|
||||||
|
// T+1 盘中盈利统一按 T+1 最高价是否高于 T 日最高价判断。
|
||||||
|
profitable := *row.NextHigh > *row.THigh
|
||||||
|
item.NextIntradayProfitable = &profitable
|
||||||
|
profitPct := roundSecondCalPct((*row.NextHigh - *row.THigh) / *row.THigh * 100)
|
||||||
|
item.NextHighProfitPct = &profitPct
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// accumulateSecondCalDay 把单只股票累加到对应交易日统计中。
|
||||||
|
func accumulateSecondCalDay(day *model.SecondCalDay, item model.SecondCalStock) {
|
||||||
|
day.SecondBoardCount++
|
||||||
|
switch item.Status {
|
||||||
|
case "limit":
|
||||||
|
day.LimitCount++
|
||||||
|
case "break":
|
||||||
|
day.BreakCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.NextIntradayProfitable != nil {
|
||||||
|
day.NextTradeDataCount++
|
||||||
|
if item.Status == "limit" {
|
||||||
|
day.LimitNextDataCount++
|
||||||
|
}
|
||||||
|
if item.Status == "break" {
|
||||||
|
day.BreakNextDataCount++
|
||||||
|
}
|
||||||
|
if *item.NextIntradayProfitable {
|
||||||
|
day.NextIntradayProfitCount++
|
||||||
|
if item.Status == "limit" {
|
||||||
|
day.LimitNextProfitCount++
|
||||||
|
}
|
||||||
|
if item.Status == "break" {
|
||||||
|
day.BreakNextProfitCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
day.Stocks = append(day.Stocks, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalizeSecondCalDay 计算单个交易日的概率字段。
|
||||||
|
func finalizeSecondCalDay(day *model.SecondCalDay) {
|
||||||
|
day.NextIntradayProfitRate = secondCalRate(day.NextIntradayProfitCount, day.NextTradeDataCount)
|
||||||
|
day.LimitNextProfitRate = secondCalRate(day.LimitNextProfitCount, day.LimitNextDataCount)
|
||||||
|
day.BreakNextProfitRate = secondCalRate(day.BreakNextProfitCount, day.BreakNextDataCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// accumulateSecondCalSummary 把单日统计累加到日期范围汇总中。
|
||||||
|
func accumulateSecondCalSummary(summary *model.SecondCalSummary, day model.SecondCalDay) {
|
||||||
|
summary.SecondBoardCount += day.SecondBoardCount
|
||||||
|
summary.LimitCount += day.LimitCount
|
||||||
|
summary.BreakCount += day.BreakCount
|
||||||
|
summary.NextTradeDataCount += day.NextTradeDataCount
|
||||||
|
summary.NextIntradayProfitCount += day.NextIntradayProfitCount
|
||||||
|
summary.LimitNextDataCount += day.LimitNextDataCount
|
||||||
|
summary.LimitNextProfitCount += day.LimitNextProfitCount
|
||||||
|
summary.BreakNextDataCount += day.BreakNextDataCount
|
||||||
|
summary.BreakNextProfitCount += day.BreakNextProfitCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalizeSecondCalSummary 计算日期范围汇总的概率字段。
|
||||||
|
func finalizeSecondCalSummary(summary *model.SecondCalSummary) {
|
||||||
|
summary.NextIntradayProfitRate = secondCalRate(summary.NextIntradayProfitCount, summary.NextTradeDataCount)
|
||||||
|
summary.LimitNextProfitRate = secondCalRate(summary.LimitNextProfitCount, summary.LimitNextDataCount)
|
||||||
|
summary.BreakNextProfitRate = secondCalRate(summary.BreakNextProfitCount, summary.BreakNextDataCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// secondCalRate 计算百分比概率并保留两位小数。
|
||||||
|
func secondCalRate(numerator, denominator int) float64 {
|
||||||
|
if denominator == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return roundSecondCalPct(float64(numerator) / float64(denominator) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// roundSecondCalPct 对二板日历中的百分比做两位小数舍入。
|
||||||
|
func roundSecondCalPct(value float64) float64 {
|
||||||
|
return math.Round(value*100) / 100
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user