1、新增二板炸板情况统计;
2、更新项目结构;
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- 服务前缀:`/api/v1`
|
- 服务前缀:`/api/v1`
|
||||||
- 响应格式:`application/json`
|
- 响应格式:`application/json`
|
||||||
- 当前项目接口总数:8
|
- 当前项目接口总数:10
|
||||||
|
|
||||||
## 2. 接口列表
|
## 2. 接口列表
|
||||||
|
|
||||||
@@ -17,7 +17,9 @@
|
|||||||
| 涨停低开分组 | `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/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) |
|
||||||
|
|
||||||
## 3. 错误响应约定
|
## 3. 错误响应约定
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# 炸板次日表现接口文档
|
||||||
|
|
||||||
|
## 1. 基本信息
|
||||||
|
|
||||||
|
- 接口名称:炸板次日表现
|
||||||
|
- 请求方法:`GET`
|
||||||
|
- 请求路径:`/api/v1/limit-board/break-next-performance`
|
||||||
|
- 接口用途:按 T 日范围查询 T-2 未涨停、T-1 涨停、T 日触及涨停价但收盘未封住的炸板股票,并返回 T+1 交易日表现。
|
||||||
|
- 导出路径:`/api/v1/download/limit-board/break-next-performance-csv`
|
||||||
|
|
||||||
|
## 2. 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `tradeStartDate` | `string` | 是 | 无 | T 日起始日期,格式 `YYYY-MM-DD` |
|
||||||
|
| `tradeEndDate` | `string` | 是 | 无 | T 日结束日期,格式 `YYYY-MM-DD` |
|
||||||
|
| `page` | `int` | 否 | `1` | 页码 |
|
||||||
|
| `pageSize` | `int` | 否 | `20` | 每页数量 |
|
||||||
|
| `tOpenGainLt101` | `bool` | 否 | 无 | 筛选 T 日开盘价是否小于 T-1 收盘价的 101%,支持别名 `tOpenLessThan101`、`t_open_gain_lt_101` |
|
||||||
|
| `breakTimeBefore1100` | `bool` | 否 | 无 | 筛选 T 日炸板时间是否在 `11:00:00` 前,空炸板时间按 `true` 处理,支持别名 `tBreakTimeBefore1100`、`break_time_before_1100` |
|
||||||
|
|
||||||
|
兼容 `start_date`、`end_date` 作为日期参数别名。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/limit-board/break-next-performance?tradeStartDate=2026-05-07&tradeEndDate=2026-05-07&tOpenGainLt101=true&breakTimeBefore1100=true&page=1&pageSize=20
|
||||||
|
```
|
||||||
|
|
||||||
|
导出示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/download/limit-board/break-next-performance-csv?tradeStartDate=2026-05-07&tradeEndDate=2026-05-07&tOpenGainLt101=true&breakTimeBefore1100=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 统计规则
|
||||||
|
|
||||||
|
- 以 `trade_calendar` 中 `exchange = 'SSE'` 且 `is_open = 1` 的记录推导 T-2、T-1、T、T+1 交易日。
|
||||||
|
- 样本池来自 `stock_limit_combined_data`:T 日必须为炸板记录,即 `final_status = 2`;T-1 必须为涨停记录,即 `final_status = 1`;T-2 同一股票不能存在 `final_status = 1`。
|
||||||
|
- T 日和 T+1 日 OHLC 来自 `stock_daily`。
|
||||||
|
- 股票板块来自 `stock_limit_combined_data.market`。
|
||||||
|
- T 日开盘涨幅以 T-1 收盘价为基准计算。
|
||||||
|
- 如果 T 日有 Tushare 炸板记录,则返回具体的 Tushare 炸板时间 `tushare_first_time` 并格式化为 `HH:MM:SS`;无 Tushare 炸板记录或无具体时间时 `first_time` 为空。
|
||||||
|
- T 日炸板时间为空时,`break_time_before_1100` 默认返回 `true`。
|
||||||
|
- T+1 开盘涨幅、最高获利、最大亏损、收盘涨跌幅均以 T 日收盘价为基准计算。
|
||||||
|
- T+1 盘中是否可以获利表示:T+1 盘中最高价是否能弥补 T 日最高价买入后到 T 日收盘价的差价,按 `next_high >= t_high` 判断;缺少 T+1 最高价、T 日最高价或 T 日收盘价时返回 `null`。
|
||||||
|
|
||||||
|
## 4. 响应字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `trade_start_date` | `string` | 查询起始 T 日 |
|
||||||
|
| `trade_end_date` | `string` | 查询结束 T 日 |
|
||||||
|
| `page` | `int` | 当前页 |
|
||||||
|
| `page_size` | `int` | 每页数量 |
|
||||||
|
| `total` | `int` | 命中总数 |
|
||||||
|
| `total_pages` | `int` | 总页数 |
|
||||||
|
| `summary.break_count` | `int` | T 日炸板股票总数 |
|
||||||
|
| `summary.next_trade_data_count` | `int` | 有 T+1 行情数据的股票数 |
|
||||||
|
| `list` | `array` | 明细列表 |
|
||||||
|
|
||||||
|
明细核心字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `trade_date` | `string` | T 日 |
|
||||||
|
| `ts_code` / `symbol` / `name` | `string` | 股票标识 |
|
||||||
|
| `market` / `industry` | `string` | 板块和行业 |
|
||||||
|
| `t_open` / `t_high` / `t_close` | `number \| null` | T 日行情 |
|
||||||
|
| `t_open_pct_chg` | `number \| null` | T 日开盘涨幅,基于 T-1 收盘价 |
|
||||||
|
| `t_open_gain_lt_101` | `boolean` | T 日开盘价是否小于 T-1 收盘价的 101% |
|
||||||
|
| `first_time` | `string` | T 日 Tushare 炸板时间,格式化为 `HH:MM:SS`,无 Tushare 炸板记录或无具体时间时为空 |
|
||||||
|
| `break_time_before_1100` | `boolean` | T 日炸板时间是否在 `11:00:00` 前;空炸板时间默认为 `true` |
|
||||||
|
| `has_tushare_break_data` | `boolean` | T 日是否存在 Tushare 炸板记录,即 `tushare_limit_type = 'Z'` |
|
||||||
|
| `next_open` / `next_high` / `next_low` / `next_close` | `number \| null` | T+1 行情 |
|
||||||
|
| `next_open_pct_chg` | `number \| null` | T+1 开盘涨幅,基于 T 日收盘价 |
|
||||||
|
| `next_intraday_profitable` | `boolean \| null` | T+1 盘中是否可以获利,按 T+1 最高价是否能弥补 T 日最高价买入后到 T 日收盘价的差价判断,即 `next_high >= t_high` |
|
||||||
|
| `next_high_profit_pct` | `number \| null` | T+1 最高获利百分比,基于 T 日收盘价 |
|
||||||
|
| `next_low_loss_pct` | `number \| null` | T+1 最大亏损百分比,基于 T 日收盘价 |
|
||||||
|
| `next_close_pct_chg` | `number \| null` | T+1 收盘涨跌幅,基于 T 日收盘价 |
|
||||||
|
|
||||||
|
## 5. 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"trade_start_date": "2026-05-07",
|
||||||
|
"trade_end_date": "2026-05-07",
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"total": 3,
|
||||||
|
"total_pages": 1,
|
||||||
|
"summary": {
|
||||||
|
"break_count": 3,
|
||||||
|
"next_trade_data_count": 3
|
||||||
|
},
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"trade_date": "2026-05-07",
|
||||||
|
"symbol": "000060",
|
||||||
|
"ts_code": "000060.SZ",
|
||||||
|
"name": "中金岭南",
|
||||||
|
"market": "主板",
|
||||||
|
"industry": "铅锌",
|
||||||
|
"t_open": 7.71,
|
||||||
|
"t_open_pct_chg": 3.77,
|
||||||
|
"t_open_gain_lt_101": false,
|
||||||
|
"t_high": 8.17,
|
||||||
|
"t_close": 8.08,
|
||||||
|
"t_pct_chg": 8.7483,
|
||||||
|
"first_time": "13:55:30",
|
||||||
|
"break_time_before_1100": false,
|
||||||
|
"has_tushare_break_data": true,
|
||||||
|
"next_open": 7.81,
|
||||||
|
"next_high": 8.23,
|
||||||
|
"next_low": 7.77,
|
||||||
|
"next_close": 7.95,
|
||||||
|
"next_open_pct_chg": -3.34,
|
||||||
|
"next_intraday_profitable": true,
|
||||||
|
"next_high_profit_pct": 1.86,
|
||||||
|
"next_low_loss_pct": -3.84,
|
||||||
|
"next_close_pct_chg": -1.61
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. CSV 导出
|
||||||
|
|
||||||
|
- 响应类型:`text/csv; charset=utf-8`
|
||||||
|
- 响应头:`Content-Disposition: attachment; filename="limit_break_next_performance_YYYYMMDD_YYYYMMDD_daily_detail.csv"`
|
||||||
|
- 文件包含 UTF-8 BOM,便于 Excel 直接打开中文列名。
|
||||||
|
- 导出接口复用列表接口的日期、`tOpenGainLt101`、`breakTimeBefore1100` 筛选条件,不使用分页参数。
|
||||||
|
|
||||||
|
列顺序:
|
||||||
|
|
||||||
|
| 列名 | 来源字段 |
|
||||||
|
| --- | --- |
|
||||||
|
| `T日` | `trade_date` |
|
||||||
|
| `股票代码` | `ts_code` |
|
||||||
|
| `股票简称` | `name` |
|
||||||
|
| `T涨跌幅` | `t_pct_chg` |
|
||||||
|
| `T开盘<1%` | `t_open_gain_lt_101`,导出为 `是`/`否` |
|
||||||
|
| `tushare炸板时间` | `first_time` |
|
||||||
|
| `11点前炸板` | `break_time_before_1100`,导出为 `是`/`否` |
|
||||||
|
| `T+1盘中可盈利` | `next_intraday_profitable`,导出为 `是`/`否`/`无次日数据` |
|
||||||
|
| `T+1最高获利` | `next_high_profit_pct` |
|
||||||
|
| `T+1开盘涨幅` | `next_open_pct_chg` |
|
||||||
|
| `T+1收盘涨跌` | `next_close_pct_chg` |
|
||||||
|
| `市场` | `market` |
|
||||||
|
| `行业` | `industry` |
|
||||||
|
| `T开盘` | `t_open` |
|
||||||
|
| `T开盘涨幅` | `t_open_pct_chg` |
|
||||||
|
| `T最高` | `t_high` |
|
||||||
|
| `T收盘` | `t_close` |
|
||||||
|
| `tushare是否炸板` | `has_tushare_break_data`,导出为 `是`/`否` |
|
||||||
|
| `T+1开盘` | `next_open` |
|
||||||
|
| `T+1最高` | `next_high` |
|
||||||
|
| `T+1最低` | `next_low` |
|
||||||
|
| `T+1收盘` | `next_close` |
|
||||||
|
| `T+1最大亏损` | `next_low_loss_pct` |
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"level_go/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *LimitBoardHandler) DownloadBreakNextPerformanceCSV(c *gin.Context) {
|
||||||
|
req, err := parseLimitBreakNextPerformanceRequest(c, false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _, err := h.Service.ListLimitBreakNextPerformanceStocks(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("limit_break_next_performance_%s_%s_daily_detail.csv",
|
||||||
|
strings.ReplaceAll(req.TradeStartDate, "-", ""),
|
||||||
|
strings.ReplaceAll(req.TradeEndDate, "-", ""),
|
||||||
|
)
|
||||||
|
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
|
||||||
|
if _, err := c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := csv.NewWriter(c.Writer)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
_ = writer.Write(breakNextPerformanceCSVHeaders())
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := writer.Write(breakNextPerformanceCSVRecord(row)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func breakNextPerformanceCSVHeaders() []string {
|
||||||
|
return []string{
|
||||||
|
"T日",
|
||||||
|
"股票代码",
|
||||||
|
"股票简称",
|
||||||
|
"T涨跌幅",
|
||||||
|
"T开盘<1%",
|
||||||
|
"tushare炸板时间",
|
||||||
|
"11点前炸板",
|
||||||
|
"T+1盘中可盈利",
|
||||||
|
"T+1最高获利",
|
||||||
|
"T+1开盘涨幅",
|
||||||
|
"T+1收盘涨跌",
|
||||||
|
"市场",
|
||||||
|
"行业",
|
||||||
|
"T开盘",
|
||||||
|
"T开盘涨幅",
|
||||||
|
"T最高",
|
||||||
|
"T收盘",
|
||||||
|
"tushare是否炸板",
|
||||||
|
"T+1开盘",
|
||||||
|
"T+1最高",
|
||||||
|
"T+1最低",
|
||||||
|
"T+1收盘",
|
||||||
|
"T+1最大亏损",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func breakNextPerformanceCSVRecord(row model.LimitBreakNextPerformanceStock) []string {
|
||||||
|
return []string{
|
||||||
|
row.TradeDate,
|
||||||
|
row.TsCode,
|
||||||
|
row.Name,
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.TPctChg),
|
||||||
|
breakNextPerformanceBoolLabel(row.TOpenGainLt101),
|
||||||
|
row.FirstTime,
|
||||||
|
breakNextPerformanceBoolLabel(row.BreakTimeBefore1100),
|
||||||
|
breakNextPerformanceBoolPtrLabel(row.NextIntradayProfitable),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextHighProfitPct),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextOpenPctChg),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextClosePctChg),
|
||||||
|
row.Market,
|
||||||
|
row.Industry,
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.TOpen),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.TOpenPctChg),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.THigh),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.TClose),
|
||||||
|
breakNextPerformanceBoolLabel(row.HasTushareBreakData),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextOpen),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextHigh),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextLow),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextClose),
|
||||||
|
breakNextPerformanceFormatFloatPtr(row.NextLowLossPct),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func breakNextPerformanceFormatFloatPtr(value *float64) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strconv.FormatFloat(*value, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func breakNextPerformanceBoolLabel(value bool) string {
|
||||||
|
if value {
|
||||||
|
return "是"
|
||||||
|
}
|
||||||
|
return "否"
|
||||||
|
}
|
||||||
|
|
||||||
|
func breakNextPerformanceBoolPtrLabel(value *bool) string {
|
||||||
|
if value == nil {
|
||||||
|
return "无次日数据"
|
||||||
|
}
|
||||||
|
return breakNextPerformanceBoolLabel(*value)
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"level_go/internal/model"
|
"level_go/internal/model"
|
||||||
@@ -48,6 +51,90 @@ func (h *LimitBoardHandler) GetOpenLowerGroups(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, resp)
|
c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *LimitBoardHandler) GetBreakNextPerformance(c *gin.Context) {
|
||||||
|
req, err := parseLimitBreakNextPerformanceRequest(c, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.Service.GetLimitBreakNextPerformance(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLimitBreakNextPerformanceRequest(c *gin.Context, includePagination bool) (model.LimitBreakNextPerformanceRequest, error) {
|
||||||
|
req := model.LimitBreakNextPerformanceRequest{
|
||||||
|
TradeStartDate: strings.TrimSpace(c.DefaultQuery("tradeStartDate", c.Query("start_date"))),
|
||||||
|
TradeEndDate: strings.TrimSpace(c.DefaultQuery("tradeEndDate", c.Query("end_date"))),
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if includePagination {
|
||||||
|
pageText := strings.TrimSpace(c.DefaultQuery("page", "1"))
|
||||||
|
page, err := strconv.Atoi(pageText)
|
||||||
|
if err != nil || page <= 0 {
|
||||||
|
return req, errors.New("page 必须为正整数")
|
||||||
|
}
|
||||||
|
req.Page = page
|
||||||
|
|
||||||
|
pageSizeText := strings.TrimSpace(c.DefaultQuery("pageSize", "20"))
|
||||||
|
pageSize, err := strconv.Atoi(pageSizeText)
|
||||||
|
if err != nil || pageSize <= 0 {
|
||||||
|
return req, errors.New("pageSize 必须为正整数")
|
||||||
|
}
|
||||||
|
req.PageSize = pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
tOpenGainLt101, err := parseOptionalBoolQuery(c, "tOpenGainLt101", "tOpenLessThan101", "t_open_gain_lt_101")
|
||||||
|
if err != nil {
|
||||||
|
return req, errors.New("tOpenGainLt101 必须为布尔值")
|
||||||
|
}
|
||||||
|
req.TOpenGainLt101 = tOpenGainLt101
|
||||||
|
|
||||||
|
breakTimeBefore1100, err := parseOptionalBoolQuery(c, "breakTimeBefore1100", "tBreakTimeBefore1100", "break_time_before_1100")
|
||||||
|
if err != nil {
|
||||||
|
return req, errors.New("breakTimeBefore1100 必须为布尔值")
|
||||||
|
}
|
||||||
|
req.BreakTimeBefore1100 = breakTimeBefore1100
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptionalBoolQuery(c *gin.Context, keys ...string) (*bool, error) {
|
||||||
|
for _, key := range keys {
|
||||||
|
valueText, exists := c.GetQuery(key)
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
valueText = strings.TrimSpace(valueText)
|
||||||
|
if valueText == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseBool(valueText)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &value, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *LimitBoardHandler) GetOpenThresholdGroups(c *gin.Context) {
|
func (h *LimitBoardHandler) GetOpenThresholdGroups(c *gin.Context) {
|
||||||
var req model.LimitBoardRequest
|
var req model.LimitBoardRequest
|
||||||
if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" {
|
if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" {
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ type LimitBoardRequest struct {
|
|||||||
TradeDate string `form:"trade_date" json:"trade_date"`
|
TradeDate string `form:"trade_date" json:"trade_date"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LimitBreakNextPerformanceRequest struct {
|
||||||
|
TradeStartDate string `form:"tradeStartDate" json:"trade_start_date"`
|
||||||
|
TradeEndDate string `form:"tradeEndDate" json:"trade_end_date"`
|
||||||
|
Page int `form:"page" json:"page"`
|
||||||
|
PageSize int `form:"pageSize" json:"page_size"`
|
||||||
|
TOpenGainLt101 *bool `json:"t_open_gain_lt_101,omitempty"`
|
||||||
|
BreakTimeBefore1100 *bool `json:"break_time_before_1100,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type LimitBoardStock struct {
|
type LimitBoardStock struct {
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
TsCode string `json:"ts_code"`
|
TsCode string `json:"ts_code"`
|
||||||
@@ -92,6 +101,33 @@ type LimitOpenThresholdStock struct {
|
|||||||
Vol *int64 `json:"vol"`
|
Vol *int64 `json:"vol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LimitBreakNextPerformanceStock struct {
|
||||||
|
TradeDate string `json:"trade_date"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
TsCode string `json:"ts_code"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Market string `json:"market"`
|
||||||
|
Industry string `json:"industry"`
|
||||||
|
TOpen *float64 `json:"t_open"`
|
||||||
|
TOpenPctChg *float64 `json:"t_open_pct_chg"`
|
||||||
|
TOpenGainLt101 bool `json:"t_open_gain_lt_101"`
|
||||||
|
THigh *float64 `json:"t_high"`
|
||||||
|
TClose *float64 `json:"t_close"`
|
||||||
|
TPctChg *float64 `json:"t_pct_chg"`
|
||||||
|
FirstTime string `json:"first_time"`
|
||||||
|
BreakTimeBefore1100 bool `json:"break_time_before_1100"`
|
||||||
|
HasTushareBreakData bool `json:"has_tushare_break_data"`
|
||||||
|
NextOpen *float64 `json:"next_open"`
|
||||||
|
NextHigh *float64 `json:"next_high"`
|
||||||
|
NextLow *float64 `json:"next_low"`
|
||||||
|
NextClose *float64 `json:"next_close"`
|
||||||
|
NextOpenPctChg *float64 `json:"next_open_pct_chg"`
|
||||||
|
NextIntradayProfitable *bool `json:"next_intraday_profitable"`
|
||||||
|
NextHighProfitPct *float64 `json:"next_high_profit_pct"`
|
||||||
|
NextLowLossPct *float64 `json:"next_low_loss_pct"`
|
||||||
|
NextClosePctChg *float64 `json:"next_close_pct_chg"`
|
||||||
|
}
|
||||||
|
|
||||||
type LimitOpenCompareGroup struct {
|
type LimitOpenCompareGroup struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
@@ -121,6 +157,11 @@ type LimitOpenThresholdSummary struct {
|
|||||||
ThresholdRatio float64 `json:"threshold_ratio"`
|
ThresholdRatio float64 `json:"threshold_ratio"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LimitBreakNextPerformanceSummary struct {
|
||||||
|
BreakCount int `json:"break_count"`
|
||||||
|
NextTradeDataCount int `json:"next_trade_data_count"`
|
||||||
|
}
|
||||||
|
|
||||||
type LimitOpenCompareResponse struct {
|
type LimitOpenCompareResponse struct {
|
||||||
TradeDate string `json:"trade_date"`
|
TradeDate string `json:"trade_date"`
|
||||||
PreviousTradeDate string `json:"previous_trade_date"`
|
PreviousTradeDate string `json:"previous_trade_date"`
|
||||||
@@ -129,6 +170,17 @@ type LimitOpenCompareResponse struct {
|
|||||||
Groups []LimitOpenCompareGroup `json:"groups"`
|
Groups []LimitOpenCompareGroup `json:"groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LimitBreakNextPerformanceResponse struct {
|
||||||
|
TradeStartDate string `json:"trade_start_date"`
|
||||||
|
TradeEndDate string `json:"trade_end_date"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
Summary LimitBreakNextPerformanceSummary `json:"summary"`
|
||||||
|
List []LimitBreakNextPerformanceStock `json:"list"`
|
||||||
|
}
|
||||||
|
|
||||||
type LimitOpenThresholdResponse struct {
|
type LimitOpenThresholdResponse struct {
|
||||||
TradeDate string `json:"trade_date"`
|
TradeDate string `json:"trade_date"`
|
||||||
PreviousTradeDate string `json:"previous_trade_date"`
|
PreviousTradeDate string `json:"previous_trade_date"`
|
||||||
|
|||||||
@@ -34,11 +34,13 @@ 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("/break-next-performance", limitBoardApi.GetBreakNextPerformance)
|
||||||
}
|
}
|
||||||
|
|
||||||
download := v1.Group("/download")
|
download := v1.Group("/download")
|
||||||
{
|
{
|
||||||
download.GET("/limit-board/second-board-profit-csv", limitBoardApi.DownloadSecondBoardProfitCSV)
|
download.GET("/limit-board/second-board-profit-csv", limitBoardApi.DownloadSecondBoardProfitCSV)
|
||||||
|
download.GET("/limit-board/break-next-performance-csv", limitBoardApi.DownloadBreakNextPerformanceCSV)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"level_go/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type limitBreakNextPerformanceRow struct {
|
||||||
|
TradeDate string `gorm:"column:trade_date"`
|
||||||
|
PreviousTradeDate string `gorm:"column:previous_trade_date"`
|
||||||
|
PreviousTwoTradeDate string `gorm:"column:previous_two_trade_date"`
|
||||||
|
NextTradeDate string `gorm:"column:next_trade_date"`
|
||||||
|
Symbol string `gorm:"column:symbol"`
|
||||||
|
TsCode string `gorm:"column:ts_code"`
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
Market string `gorm:"column:market"`
|
||||||
|
Industry string `gorm:"column:industry"`
|
||||||
|
PreviousClose *float64 `gorm:"column:previous_close"`
|
||||||
|
TPreClose *float64 `gorm:"column:t_pre_close"`
|
||||||
|
TOpen *float64 `gorm:"column:t_open"`
|
||||||
|
THigh *float64 `gorm:"column:t_high"`
|
||||||
|
TClose *float64 `gorm:"column:t_close"`
|
||||||
|
TPctChg *float64 `gorm:"column:t_pct_chg"`
|
||||||
|
FirstTime *string `gorm:"column:first_time"`
|
||||||
|
HasTushareBreakData bool `gorm:"column:has_tushare_break_data"`
|
||||||
|
NextOpen *float64 `gorm:"column:next_open"`
|
||||||
|
NextHigh *float64 `gorm:"column:next_high"`
|
||||||
|
NextLow *float64 `gorm:"column:next_low"`
|
||||||
|
NextClose *float64 `gorm:"column:next_close"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) GetLimitBreakNextPerformance(req model.LimitBreakNextPerformanceRequest) (model.LimitBreakNextPerformanceResponse, error) {
|
||||||
|
resp := model.LimitBreakNextPerformanceResponse{
|
||||||
|
TradeStartDate: req.TradeStartDate,
|
||||||
|
TradeEndDate: req.TradeEndDate,
|
||||||
|
Page: req.Page,
|
||||||
|
PageSize: req.PageSize,
|
||||||
|
List: []model.LimitBreakNextPerformanceStock{},
|
||||||
|
}
|
||||||
|
|
||||||
|
items, nextTradeDataCount, err := s.ListLimitBreakNextPerformanceStocks(req)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Total = len(items)
|
||||||
|
if resp.Total > 0 {
|
||||||
|
resp.TotalPages = (resp.Total + req.PageSize - 1) / req.PageSize
|
||||||
|
}
|
||||||
|
resp.Summary = model.LimitBreakNextPerformanceSummary{
|
||||||
|
BreakCount: resp.Total,
|
||||||
|
NextTradeDataCount: nextTradeDataCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
start := (req.Page - 1) * req.PageSize
|
||||||
|
if start > resp.Total {
|
||||||
|
start = resp.Total
|
||||||
|
}
|
||||||
|
end := start + req.PageSize
|
||||||
|
if end > resp.Total {
|
||||||
|
end = resp.Total
|
||||||
|
}
|
||||||
|
resp.List = items[start:end]
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) ListLimitBreakNextPerformanceStocks(req model.LimitBreakNextPerformanceRequest) ([]model.LimitBreakNextPerformanceStock, int, error) {
|
||||||
|
rows, err := s.loadLimitBreakNextPerformanceRows(req.TradeStartDate, req.TradeEndDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]model.LimitBreakNextPerformanceStock, 0, len(rows))
|
||||||
|
nextTradeDataCount := 0
|
||||||
|
for _, row := range rows {
|
||||||
|
item := buildLimitBreakNextPerformanceStock(row)
|
||||||
|
if req.TOpenGainLt101 != nil && item.TOpenGainLt101 != *req.TOpenGainLt101 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if req.BreakTimeBefore1100 != nil && item.BreakTimeBefore1100 != *req.BreakTimeBefore1100 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.NextOpen != nil || item.NextHigh != nil || item.NextLow != nil || item.NextClose != nil {
|
||||||
|
nextTradeDataCount++
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, nextTradeDataCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) loadLimitBreakNextPerformanceRows(startDate, endDate string) ([]limitBreakNextPerformanceRow, 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
|
||||||
|
DATE_FORMAT(c.trade_date, '%Y-%m-%d') AS trade_date,
|
||||||
|
DATE_FORMAT(c.previous_trade_date, '%Y-%m-%d') AS previous_trade_date,
|
||||||
|
DATE_FORMAT(c.previous_two_trade_date, '%Y-%m-%d') AS previous_two_trade_date,
|
||||||
|
COALESCE(DATE_FORMAT(c.next_trade_date, '%Y-%m-%d'), '') AS next_trade_date,
|
||||||
|
l.symbol,
|
||||||
|
l.ts_code,
|
||||||
|
l.name,
|
||||||
|
COALESCE(NULLIF(l.market, ''), '') AS market,
|
||||||
|
COALESCE(NULLIF(l.tushare_industry, ''), NULLIF(sb.industry, ''), '') AS industry,
|
||||||
|
COALESCE(d.pre_close, l.calc_pre_close, pd.close, pl.calc_close, pl.tushare_close) AS previous_close,
|
||||||
|
COALESCE(d.pre_close, l.calc_pre_close, pd.close, pl.calc_close, pl.tushare_close) AS t_pre_close,
|
||||||
|
d.open AS t_open,
|
||||||
|
d.high AS t_high,
|
||||||
|
COALESCE(l.calc_close, l.tushare_close, d.close) AS t_close,
|
||||||
|
COALESCE(l.tushare_pct_chg, d.pct_chg) AS t_pct_chg,
|
||||||
|
CASE
|
||||||
|
WHEN l.tushare_limit_type = 'Z'
|
||||||
|
THEN NULLIF(NULLIF(TRIM(l.tushare_first_time), ''), 'None')
|
||||||
|
ELSE NULL
|
||||||
|
END AS first_time,
|
||||||
|
(l.tushare_limit_type = 'Z') AS has_tushare_break_data,
|
||||||
|
nd.open AS next_open,
|
||||||
|
nd.high AS next_high,
|
||||||
|
nd.low AS next_low,
|
||||||
|
nd.close AS next_close
|
||||||
|
FROM open_calendar c
|
||||||
|
INNER JOIN stock_limit_combined_data l
|
||||||
|
ON l.trade_date = c.trade_date AND l.final_status = 2
|
||||||
|
INNER JOIN stock_limit_combined_data pl
|
||||||
|
ON pl.ts_code = l.ts_code AND pl.trade_date = c.previous_trade_date AND pl.final_status = 1
|
||||||
|
LEFT JOIN stock_limit_combined_data ppl
|
||||||
|
ON ppl.ts_code = l.ts_code AND ppl.trade_date = c.previous_two_trade_date AND ppl.final_status = 1
|
||||||
|
LEFT JOIN stock_daily d
|
||||||
|
ON d.ts_code = l.ts_code AND d.trade_date = c.trade_date
|
||||||
|
LEFT JOIN stock_daily pd
|
||||||
|
ON pd.ts_code = l.ts_code AND pd.trade_date = c.previous_trade_date
|
||||||
|
LEFT JOIN stock_daily nd
|
||||||
|
ON nd.ts_code = l.ts_code AND nd.trade_date = c.next_trade_date
|
||||||
|
LEFT JOIN stock_basic sb
|
||||||
|
ON sb.ts_code = l.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
|
||||||
|
)
|
||||||
|
SELECT *
|
||||||
|
FROM candidate
|
||||||
|
WHERE previous_close IS NOT NULL
|
||||||
|
AND t_close IS NOT NULL
|
||||||
|
ORDER BY trade_date ASC, ts_code ASC`
|
||||||
|
|
||||||
|
var rows []limitBreakNextPerformanceRow
|
||||||
|
err := s.DB.Raw(query, startDate, endDate).Scan(&rows).Error
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLimitBreakNextPerformanceStock(row limitBreakNextPerformanceRow) model.LimitBreakNextPerformanceStock {
|
||||||
|
item := model.LimitBreakNextPerformanceStock{
|
||||||
|
TradeDate: row.TradeDate,
|
||||||
|
Symbol: row.Symbol,
|
||||||
|
TsCode: row.TsCode,
|
||||||
|
Name: row.Name,
|
||||||
|
Market: row.Market,
|
||||||
|
Industry: row.Industry,
|
||||||
|
TOpen: row.TOpen,
|
||||||
|
THigh: row.THigh,
|
||||||
|
TClose: row.TClose,
|
||||||
|
TPctChg: row.TPctChg,
|
||||||
|
HasTushareBreakData: row.HasTushareBreakData,
|
||||||
|
NextOpen: row.NextOpen,
|
||||||
|
NextHigh: row.NextHigh,
|
||||||
|
NextLow: row.NextLow,
|
||||||
|
NextClose: row.NextClose,
|
||||||
|
}
|
||||||
|
if row.FirstTime != nil {
|
||||||
|
item.FirstTime = formatLimitTime(*row.FirstTime)
|
||||||
|
}
|
||||||
|
item.BreakTimeBefore1100 = isLimitBreakTimeBeforeEleven(item.FirstTime)
|
||||||
|
if row.TPreClose != nil && *row.TPreClose > 0 {
|
||||||
|
item.TOpenPctChg = pctChangePtr(row.TOpen, *row.TPreClose)
|
||||||
|
if row.TOpen != nil {
|
||||||
|
item.TOpenGainLt101 = *row.TOpen < *row.TPreClose*1.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if row.TClose != nil && *row.TClose > 0 {
|
||||||
|
item.NextOpenPctChg = pctChangePtr(row.NextOpen, *row.TClose)
|
||||||
|
item.NextHighProfitPct = pctChangePtr(row.NextHigh, *row.TClose)
|
||||||
|
item.NextLowLossPct = pctChangePtr(row.NextLow, *row.TClose)
|
||||||
|
item.NextClosePctChg = pctChangePtr(row.NextClose, *row.TClose)
|
||||||
|
if row.NextHigh != nil && row.THigh != nil {
|
||||||
|
nextIntradayProfitable := *row.NextHigh >= *row.THigh
|
||||||
|
item.NextIntradayProfitable = &nextIntradayProfitable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func pctChangePtr(value *float64, base float64) *float64 {
|
||||||
|
if value == nil || base == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := roundLimitBreakPct((*value - base) / base * 100)
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundLimitBreakPct(value float64) float64 {
|
||||||
|
return math.Round(value*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatLimitTime(value string) string {
|
||||||
|
timeText := strings.TrimSpace(value)
|
||||||
|
if timeText == "" || strings.EqualFold(timeText, "none") || strings.EqualFold(timeText, "null") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.Contains(timeText, ":") {
|
||||||
|
return timeText
|
||||||
|
}
|
||||||
|
switch len(timeText) {
|
||||||
|
case 4:
|
||||||
|
timeText += "00"
|
||||||
|
case 5:
|
||||||
|
timeText = "0" + timeText
|
||||||
|
case 6:
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return timeText[0:2] + ":" + timeText[2:4] + ":" + timeText[4:6]
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLimitBreakTimeBeforeEleven(limitTime string) bool {
|
||||||
|
if strings.TrimSpace(limitTime) == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isBeforeEleven(&limitTime)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBuildLimitBreakNextPerformanceStockIntradayProfitableUsesTHigh(t *testing.T) {
|
||||||
|
tClose := 9.50
|
||||||
|
tHigh := 10.00
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nextHigh *float64
|
||||||
|
want *bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "next high below T high cannot cover T high buy loss",
|
||||||
|
nextHigh: float64Ptr(9.99),
|
||||||
|
want: boolPtr(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "next high equal T high covers T high buy loss",
|
||||||
|
nextHigh: float64Ptr(10.00),
|
||||||
|
want: boolPtr(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing next high returns nil",
|
||||||
|
nextHigh: nil,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
item := buildLimitBreakNextPerformanceStock(limitBreakNextPerformanceRow{
|
||||||
|
TClose: &tClose,
|
||||||
|
THigh: &tHigh,
|
||||||
|
NextHigh: tt.nextHigh,
|
||||||
|
})
|
||||||
|
|
||||||
|
if tt.want == nil {
|
||||||
|
if item.NextIntradayProfitable != nil {
|
||||||
|
t.Fatalf("NextIntradayProfitable = %v, want nil", *item.NextIntradayProfitable)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if item.NextIntradayProfitable == nil {
|
||||||
|
t.Fatalf("NextIntradayProfitable = nil, want %v", *tt.want)
|
||||||
|
}
|
||||||
|
if *item.NextIntradayProfitable != *tt.want {
|
||||||
|
t.Fatalf("NextIntradayProfitable = %v, want %v", *item.NextIntradayProfitable, *tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func float64Ptr(value float64) *float64 {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolPtr(value bool) *bool {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"level_go/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *LimitBoardService) GetLimitOpenCompareGroups(tradeDate string) (model.LimitOpenCompareResponse, error) {
|
||||||
|
resp := model.LimitOpenCompareResponse{TradeDate: tradeDate}
|
||||||
|
|
||||||
|
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if dateIndex == -1 {
|
||||||
|
resp.Groups = buildLimitOpenCompareGroups(nil, nil)
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
previousTradeDate := ""
|
||||||
|
if dateIndex > 0 {
|
||||||
|
previousTradeDate = tradeDates[dateIndex-1]
|
||||||
|
}
|
||||||
|
resp.PreviousTradeDate = previousTradeDate
|
||||||
|
|
||||||
|
previousTwoTradeDate := ""
|
||||||
|
if dateIndex > 1 {
|
||||||
|
previousTwoTradeDate = tradeDates[dateIndex-2]
|
||||||
|
}
|
||||||
|
resp.PreviousTwoTradeDate = previousTwoTradeDate
|
||||||
|
|
||||||
|
stocks, err := s.loadLimitOpenCompareStocks(tradeDate, previousTradeDate, previousTwoTradeDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
openLowerStocks := make([]model.LimitOpenCompareStock, 0)
|
||||||
|
otherStocks := make([]model.LimitOpenCompareStock, 0)
|
||||||
|
firstToSecondBoardCount := 0
|
||||||
|
firstSecondBeforeElevenCount := 0
|
||||||
|
for _, stock := range stocks {
|
||||||
|
if stock.IsFirstToSecondBoard {
|
||||||
|
stock.HighlightLabel = "首板进二板"
|
||||||
|
firstToSecondBoardCount++
|
||||||
|
}
|
||||||
|
isOpenLower := previousTradeDate != "" && stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.PreviousClose
|
||||||
|
if isOpenLower && stock.IsFirstToSecondBoard && isBeforeEleven(stock.AkFirstLimitTime) {
|
||||||
|
stock.IsFirstSecondBeforeEleven = true
|
||||||
|
stock.HighlightLabel = "首二一一"
|
||||||
|
firstSecondBeforeElevenCount++
|
||||||
|
}
|
||||||
|
if isOpenLower {
|
||||||
|
openLowerStocks = append(openLowerStocks, stock)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
otherStocks = append(otherStocks, stock)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Summary = model.LimitOpenCompareSummary{
|
||||||
|
LimitCount: len(stocks),
|
||||||
|
OpenLowerPreviousCloseCount: len(openLowerStocks),
|
||||||
|
OthersCount: len(otherStocks),
|
||||||
|
FirstToSecondBoardCount: firstToSecondBoardCount,
|
||||||
|
FirstSecondBeforeElevenCount: firstSecondBeforeElevenCount,
|
||||||
|
}
|
||||||
|
resp.Groups = buildLimitOpenCompareGroups(openLowerStocks, otherStocks)
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBeforeEleven(limitTime *string) bool {
|
||||||
|
if limitTime == nil || *limitTime == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
timeText := strings.TrimSpace(*limitTime)
|
||||||
|
hour := 0
|
||||||
|
minute := 0
|
||||||
|
second := 0
|
||||||
|
if strings.Contains(timeText, ":") {
|
||||||
|
if n, _ := fmt.Sscanf(timeText, "%d:%d:%d", &hour, &minute, &second); n < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(timeText) < 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch len(timeText) {
|
||||||
|
case 4:
|
||||||
|
timeText += "00"
|
||||||
|
case 5:
|
||||||
|
timeText = "0" + timeText
|
||||||
|
case 6:
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
hour, err = strconv.Atoi(timeText[0:2])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
minute, err = strconv.Atoi(timeText[2:4])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
second, err = strconv.Atoi(timeText[4:6])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hour < 11
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeDate, previousTwoTradeDate string) ([]model.LimitOpenCompareStock, error) {
|
||||||
|
var stocks []model.LimitOpenCompareStock
|
||||||
|
query := `SELECT
|
||||||
|
l.symbol,
|
||||||
|
l.ts_code,
|
||||||
|
l.name,
|
||||||
|
COALESCE(l.market, '') AS market,
|
||||||
|
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
|
||||||
|
d.open IS NOT NULL AS has_open,
|
||||||
|
COALESCE(d.open, 0) AS open,
|
||||||
|
COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS close,
|
||||||
|
? AS previous_trade_date,
|
||||||
|
? AS previous_two_trade_date,
|
||||||
|
pd.close IS NOT NULL AS has_previous_close,
|
||||||
|
COALESCE(pd.close, 0) AS previous_close,
|
||||||
|
(? <> '' AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM stock_limit_combined_data ppl
|
||||||
|
WHERE ppl.ts_code = l.ts_code AND ppl.trade_date = ? AND ppl.final_status = 1
|
||||||
|
)) AS is_first_to_second_board,
|
||||||
|
COALESCE(l.calc_limit_price, 0) AS limit_price,
|
||||||
|
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
|
||||||
|
l.tushare_amount AS ak_amount,
|
||||||
|
l.tushare_float_mv AS ak_float_market_value,
|
||||||
|
l.tushare_total_mv AS ak_total_market_value,
|
||||||
|
l.tushare_turnover_ratio AS ak_turnover_rate,
|
||||||
|
l.tushare_fd_amount AS ak_seal_amount,
|
||||||
|
l.tushare_first_time AS ak_first_limit_time,
|
||||||
|
l.tushare_last_time AS ak_last_limit_time,
|
||||||
|
l.tushare_open_times AS ak_break_limit_count,
|
||||||
|
l.tushare_up_stat AS ak_limit_up_stat,
|
||||||
|
l.tushare_limit_times AS ak_limit_up_days,
|
||||||
|
l.tushare_industry AS ak_industry,
|
||||||
|
l.calc_vol AS vol
|
||||||
|
FROM stock_limit_combined_data l
|
||||||
|
LEFT JOIN stock_daily d
|
||||||
|
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||||
|
LEFT JOIN stock_daily pd
|
||||||
|
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
||||||
|
INNER JOIN stock_limit_combined_data pl
|
||||||
|
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
||||||
|
WHERE l.trade_date = ? AND l.final_status = 1
|
||||||
|
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
||||||
|
|
||||||
|
if previousTradeDate == "" {
|
||||||
|
return stocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.DB.Raw(query, previousTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error
|
||||||
|
return stocks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLimitOpenCompareGroups(openLowerStocks, otherStocks []model.LimitOpenCompareStock) []model.LimitOpenCompareGroup {
|
||||||
|
if openLowerStocks == nil {
|
||||||
|
openLowerStocks = []model.LimitOpenCompareStock{}
|
||||||
|
}
|
||||||
|
if otherStocks == nil {
|
||||||
|
otherStocks = []model.LimitOpenCompareStock{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortLimitOpenCompareStocks(openLowerStocks)
|
||||||
|
sortLimitOpenCompareStocks(otherStocks)
|
||||||
|
|
||||||
|
return []model.LimitOpenCompareGroup{
|
||||||
|
{
|
||||||
|
Key: "open_lower_previous_close",
|
||||||
|
Label: "开盘低于前收",
|
||||||
|
Count: len(openLowerStocks),
|
||||||
|
Stocks: openLowerStocks,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "others",
|
||||||
|
Label: "其他涨停股",
|
||||||
|
Count: len(otherStocks),
|
||||||
|
Stocks: otherStocks,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortLimitOpenCompareStocks(stocks []model.LimitOpenCompareStock) {
|
||||||
|
sort.Slice(stocks, func(i, j int) bool {
|
||||||
|
if stocks[i].PctChg == stocks[j].PctChg {
|
||||||
|
return stocks[i].TsCode < stocks[j].TsCode
|
||||||
|
}
|
||||||
|
return stocks[i].PctChg > stocks[j].PctChg
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"level_go/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *LimitBoardService) GetLimitOpenThresholdGroups(tradeDate string) (model.LimitOpenThresholdResponse, error) {
|
||||||
|
resp := model.LimitOpenThresholdResponse{TradeDate: tradeDate}
|
||||||
|
const thresholdRatio = 1.01
|
||||||
|
|
||||||
|
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if dateIndex == -1 {
|
||||||
|
resp.Summary.ThresholdRatio = thresholdRatio
|
||||||
|
resp.Groups = buildLimitOpenThresholdGroups(nil, nil)
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
previousTradeDate := ""
|
||||||
|
if dateIndex > 0 {
|
||||||
|
previousTradeDate = tradeDates[dateIndex-1]
|
||||||
|
}
|
||||||
|
resp.PreviousTradeDate = previousTradeDate
|
||||||
|
|
||||||
|
stocks, err := s.loadLimitOpenThresholdStocks(tradeDate, previousTradeDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
withinThresholdStocks := make([]model.LimitOpenThresholdStock, 0)
|
||||||
|
atOrAboveThresholdStocks := make([]model.LimitOpenThresholdStock, 0)
|
||||||
|
for _, stock := range stocks {
|
||||||
|
if stock.HasPreviousClose && stock.PreviousClose > 0 {
|
||||||
|
stock.OpenThresholdPrice = stock.PreviousClose * thresholdRatio
|
||||||
|
}
|
||||||
|
if stock.HasOpen && stock.HasPreviousClose && stock.PreviousClose > 0 {
|
||||||
|
stock.OpenPreviousCloseRatio = stock.Open / stock.PreviousClose
|
||||||
|
}
|
||||||
|
|
||||||
|
if stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.OpenThresholdPrice {
|
||||||
|
withinThresholdStocks = append(withinThresholdStocks, stock)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
atOrAboveThresholdStocks = append(atOrAboveThresholdStocks, stock)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Summary = model.LimitOpenThresholdSummary{
|
||||||
|
LimitCount: len(stocks),
|
||||||
|
OpenWithinThresholdCount: len(withinThresholdStocks),
|
||||||
|
OpenAtOrAboveThresholdCount: len(atOrAboveThresholdStocks),
|
||||||
|
ThresholdRatio: thresholdRatio,
|
||||||
|
}
|
||||||
|
resp.Groups = buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStocks)
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTradeDate string) ([]model.LimitOpenThresholdStock, error) {
|
||||||
|
var stocks []model.LimitOpenThresholdStock
|
||||||
|
query := `SELECT
|
||||||
|
l.symbol,
|
||||||
|
l.ts_code,
|
||||||
|
l.name,
|
||||||
|
COALESCE(l.market, '') AS market,
|
||||||
|
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
|
||||||
|
d.open IS NOT NULL AS has_open,
|
||||||
|
COALESCE(d.open, 0) AS open,
|
||||||
|
COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS close,
|
||||||
|
? AS previous_trade_date,
|
||||||
|
(pl.calc_close IS NOT NULL OR pl.tushare_close IS NOT NULL OR pd.close IS NOT NULL) AS has_previous_close,
|
||||||
|
COALESCE(pl.calc_close, pl.tushare_close, pd.close, 0) AS previous_close,
|
||||||
|
COALESCE(l.calc_limit_price, 0) AS limit_price,
|
||||||
|
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
|
||||||
|
l.tushare_amount AS ak_amount,
|
||||||
|
l.tushare_float_mv AS ak_float_market_value,
|
||||||
|
l.tushare_total_mv AS ak_total_market_value,
|
||||||
|
l.tushare_turnover_ratio AS ak_turnover_rate,
|
||||||
|
l.tushare_fd_amount AS ak_seal_amount,
|
||||||
|
l.tushare_first_time AS ak_first_limit_time,
|
||||||
|
l.tushare_last_time AS ak_last_limit_time,
|
||||||
|
l.tushare_open_times AS ak_break_limit_count,
|
||||||
|
l.tushare_up_stat AS ak_limit_up_stat,
|
||||||
|
l.tushare_limit_times AS ak_limit_up_days,
|
||||||
|
l.tushare_industry AS ak_industry,
|
||||||
|
l.calc_vol AS vol
|
||||||
|
FROM stock_limit_combined_data l
|
||||||
|
LEFT JOIN stock_daily d
|
||||||
|
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||||
|
LEFT JOIN stock_daily pd
|
||||||
|
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
||||||
|
INNER JOIN stock_limit_combined_data pl
|
||||||
|
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
||||||
|
WHERE l.trade_date = ? AND l.final_status = 1
|
||||||
|
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
||||||
|
|
||||||
|
if previousTradeDate == "" {
|
||||||
|
return stocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.DB.Raw(query, previousTradeDate, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error
|
||||||
|
return stocks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStocks []model.LimitOpenThresholdStock) []model.LimitOpenThresholdGroup {
|
||||||
|
if withinThresholdStocks == nil {
|
||||||
|
withinThresholdStocks = []model.LimitOpenThresholdStock{}
|
||||||
|
}
|
||||||
|
if atOrAboveThresholdStocks == nil {
|
||||||
|
atOrAboveThresholdStocks = []model.LimitOpenThresholdStock{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortLimitOpenThresholdStocks(withinThresholdStocks)
|
||||||
|
sortLimitOpenThresholdStocks(atOrAboveThresholdStocks)
|
||||||
|
|
||||||
|
return []model.LimitOpenThresholdGroup{
|
||||||
|
{
|
||||||
|
Key: "open_within_previous_close_101",
|
||||||
|
Label: "开盘价低于前收101%",
|
||||||
|
Count: len(withinThresholdStocks),
|
||||||
|
Stocks: withinThresholdStocks,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "open_at_or_above_previous_close_101",
|
||||||
|
Label: "开盘价大于等于前收101%",
|
||||||
|
Count: len(atOrAboveThresholdStocks),
|
||||||
|
Stocks: atOrAboveThresholdStocks,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortLimitOpenThresholdStocks(stocks []model.LimitOpenThresholdStock) {
|
||||||
|
sort.Slice(stocks, func(i, j int) bool {
|
||||||
|
if stocks[i].PctChg == stocks[j].PctChg {
|
||||||
|
return stocks[i].TsCode < stocks[j].TsCode
|
||||||
|
}
|
||||||
|
return stocks[i].PctChg > stocks[j].PctChg
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"level_go/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type limitBoardHistoryRow struct {
|
||||||
|
TsCode string `gorm:"column:ts_code"`
|
||||||
|
TradeDate string `gorm:"column:trade_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.LimitBoardResponse, error) {
|
||||||
|
resp := model.LimitBoardResponse{TradeDate: tradeDate}
|
||||||
|
|
||||||
|
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if dateIndex == -1 {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.RecentMaxBoardStats, err = s.buildRecentMaxBoardStats(tradeDates, dateIndex)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
limitStocks, err := s.loadCurrentLimitStocks(tradeDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if len(limitStocks) == 0 {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
historyMap, err := s.loadLimitHistory(limitStocks, tradeDate)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupMap, maxBoardCount := buildLimitBoardGroups(limitStocks, tradeDates, dateIndex, historyMap)
|
||||||
|
resp.Groups = sortLimitBoardGroups(groupMap)
|
||||||
|
resp.Summary = model.LimitBoardSummary{
|
||||||
|
MaxBoardCount: maxBoardCount,
|
||||||
|
LimitCount: len(limitStocks),
|
||||||
|
GroupCount: len(resp.Groups),
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIndex int) ([]model.LimitBoardDailyMax, error) {
|
||||||
|
startIndex := 0
|
||||||
|
if dateIndex >= 29 {
|
||||||
|
startIndex = dateIndex - 29
|
||||||
|
}
|
||||||
|
windowDates := tradeDates[startIndex : dateIndex+1]
|
||||||
|
if len(windowDates) == 0 {
|
||||||
|
return []model.LimitBoardDailyMax{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var history []limitBoardHistoryRow
|
||||||
|
if err := s.DB.Table(stockLimitCombinedTable).
|
||||||
|
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
||||||
|
Where("trade_date BETWEEN ? AND ? AND final_status = ?", windowDates[0], windowDates[len(windowDates)-1], 1).
|
||||||
|
Order("ts_code ASC, trade_date ASC").
|
||||||
|
Scan(&history).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dateByCode := make(map[string]map[string]struct{})
|
||||||
|
for _, item := range history {
|
||||||
|
if _, ok := dateByCode[item.TsCode]; !ok {
|
||||||
|
dateByCode[item.TsCode] = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
dateByCode[item.TsCode][item.TradeDate] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxBoardByDate := make(map[string]int, len(windowDates))
|
||||||
|
for _, tradeDate := range windowDates {
|
||||||
|
maxBoardByDate[tradeDate] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, limitDates := range dateByCode {
|
||||||
|
for idx, tradeDate := range windowDates {
|
||||||
|
if _, ok := limitDates[tradeDate]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
boardCount := 0
|
||||||
|
for j := idx; j >= 0; j-- {
|
||||||
|
if _, ok := limitDates[windowDates[j]]; !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
boardCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if boardCount > maxBoardByDate[tradeDate] {
|
||||||
|
maxBoardByDate[tradeDate] = boardCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := make([]model.LimitBoardDailyMax, 0, len(windowDates))
|
||||||
|
for _, tradeDate := range windowDates {
|
||||||
|
stats = append(stats, model.LimitBoardDailyMax{
|
||||||
|
TradeDate: tradeDate,
|
||||||
|
MaxBoardCount: maxBoardByDate[tradeDate],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.LimitBoardStock, error) {
|
||||||
|
var stocks []model.LimitBoardStock
|
||||||
|
query := `SELECT
|
||||||
|
l.symbol,
|
||||||
|
l.ts_code,
|
||||||
|
l.name,
|
||||||
|
COALESCE(l.market, '') AS market,
|
||||||
|
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
|
||||||
|
COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS close,
|
||||||
|
COALESCE(l.calc_limit_price, 0) AS limit_price,
|
||||||
|
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
|
||||||
|
l.tushare_amount AS ak_amount,
|
||||||
|
l.tushare_float_mv AS ak_float_market_value,
|
||||||
|
l.tushare_total_mv AS ak_total_market_value,
|
||||||
|
l.tushare_turnover_ratio AS ak_turnover_rate,
|
||||||
|
l.tushare_fd_amount AS ak_seal_amount,
|
||||||
|
l.tushare_first_time AS ak_first_limit_time,
|
||||||
|
l.tushare_last_time AS ak_last_limit_time,
|
||||||
|
l.tushare_open_times AS ak_break_limit_count,
|
||||||
|
l.tushare_up_stat AS ak_limit_up_stat,
|
||||||
|
l.tushare_limit_times AS ak_limit_up_days,
|
||||||
|
l.tushare_industry AS ak_industry,
|
||||||
|
l.calc_vol AS vol
|
||||||
|
FROM stock_limit_combined_data l
|
||||||
|
LEFT JOIN stock_daily d
|
||||||
|
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
||||||
|
WHERE l.trade_date = ? AND l.final_status = 1
|
||||||
|
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
||||||
|
|
||||||
|
err := s.DB.Raw(query, tradeDate).Scan(&stocks).Error
|
||||||
|
return stocks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LimitBoardService) loadLimitHistory(stocks []model.LimitBoardStock, tradeDate string) (map[string]map[string]struct{}, error) {
|
||||||
|
codes := make([]string, 0, len(stocks))
|
||||||
|
for _, stock := range stocks {
|
||||||
|
codes = append(codes, stock.TsCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var history []limitBoardHistoryRow
|
||||||
|
if err := s.DB.Table(stockLimitCombinedTable).
|
||||||
|
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
||||||
|
Where("ts_code IN ? AND trade_date <= ? AND final_status = ?", codes, tradeDate, 1).
|
||||||
|
Scan(&history).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
historyMap := make(map[string]map[string]struct{}, len(codes))
|
||||||
|
for _, item := range history {
|
||||||
|
if _, ok := historyMap[item.TsCode]; !ok {
|
||||||
|
historyMap[item.TsCode] = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
historyMap[item.TsCode][item.TradeDate] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLimitBoardGroups(stocks []model.LimitBoardStock, tradeDates []string, dateIndex int, historyMap map[string]map[string]struct{}) (map[int][]model.LimitBoardStock, int) {
|
||||||
|
groupMap := make(map[int][]model.LimitBoardStock)
|
||||||
|
maxBoardCount := 0
|
||||||
|
|
||||||
|
for i := range stocks {
|
||||||
|
boardCount := calcBoardCount(tradeDates, dateIndex, historyMap[stocks[i].TsCode])
|
||||||
|
stocks[i].BoardCount = boardCount
|
||||||
|
stocks[i].BoardLabel = boardLabel(boardCount)
|
||||||
|
stocks[i].DisplayStatus = boardLabel(boardCount)
|
||||||
|
|
||||||
|
if boardCount > maxBoardCount {
|
||||||
|
maxBoardCount = boardCount
|
||||||
|
}
|
||||||
|
groupMap[boardCount] = append(groupMap[boardCount], stocks[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupMap, maxBoardCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcBoardCount(tradeDates []string, dateIndex int, limitDates map[string]struct{}) int {
|
||||||
|
boardCount := 0
|
||||||
|
for i := dateIndex; i >= 0; i-- {
|
||||||
|
if _, ok := limitDates[tradeDates[i]]; !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
boardCount++
|
||||||
|
}
|
||||||
|
return boardCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortLimitBoardGroups(groupMap map[int][]model.LimitBoardStock) []model.LimitBoardGroup {
|
||||||
|
boardCounts := make([]int, 0, len(groupMap))
|
||||||
|
for boardCount := range groupMap {
|
||||||
|
boardCounts = append(boardCounts, boardCount)
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(sort.IntSlice(boardCounts)))
|
||||||
|
|
||||||
|
groups := make([]model.LimitBoardGroup, 0, len(boardCounts))
|
||||||
|
for _, boardCount := range boardCounts {
|
||||||
|
stocks := groupMap[boardCount]
|
||||||
|
sort.Slice(stocks, func(i, j int) bool {
|
||||||
|
if stocks[i].PctChg == stocks[j].PctChg {
|
||||||
|
return stocks[i].TsCode < stocks[j].TsCode
|
||||||
|
}
|
||||||
|
return stocks[i].PctChg > stocks[j].PctChg
|
||||||
|
})
|
||||||
|
|
||||||
|
groups = append(groups, model.LimitBoardGroup{
|
||||||
|
BoardCount: boardCount,
|
||||||
|
Label: boardLabel(boardCount),
|
||||||
|
Count: len(stocks),
|
||||||
|
Stocks: stocks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
@@ -2,12 +2,8 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"level_go/internal/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LimitBoardService struct {
|
type LimitBoardService struct {
|
||||||
@@ -16,11 +12,6 @@ type LimitBoardService struct {
|
|||||||
|
|
||||||
const stockLimitCombinedTable = "stock_limit_combined_data"
|
const stockLimitCombinedTable = "stock_limit_combined_data"
|
||||||
|
|
||||||
type limitBoardHistoryRow struct {
|
|
||||||
TsCode string `gorm:"column:ts_code"`
|
|
||||||
TradeDate string `gorm:"column:trade_date"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func boardLabel(boardCount int) string {
|
func boardLabel(boardCount int) string {
|
||||||
switch boardCount {
|
switch boardCount {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -46,272 +37,6 @@ func boardLabel(boardCount int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.LimitBoardResponse, error) {
|
|
||||||
resp := model.LimitBoardResponse{TradeDate: tradeDate}
|
|
||||||
|
|
||||||
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
if dateIndex == -1 {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.RecentMaxBoardStats, err = s.buildRecentMaxBoardStats(tradeDates, dateIndex)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
limitStocks, err := s.loadCurrentLimitStocks(tradeDate)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
if len(limitStocks) == 0 {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
historyMap, err := s.loadLimitHistory(limitStocks, tradeDate)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupMap, maxBoardCount := buildLimitBoardGroups(limitStocks, tradeDates, dateIndex, historyMap)
|
|
||||||
resp.Groups = sortLimitBoardGroups(groupMap)
|
|
||||||
resp.Summary = model.LimitBoardSummary{
|
|
||||||
MaxBoardCount: maxBoardCount,
|
|
||||||
LimitCount: len(limitStocks),
|
|
||||||
GroupCount: len(resp.Groups),
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LimitBoardService) GetLimitOpenCompareGroups(tradeDate string) (model.LimitOpenCompareResponse, error) {
|
|
||||||
resp := model.LimitOpenCompareResponse{TradeDate: tradeDate}
|
|
||||||
|
|
||||||
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
if dateIndex == -1 {
|
|
||||||
resp.Groups = buildLimitOpenCompareGroups(nil, nil)
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
previousTradeDate := ""
|
|
||||||
if dateIndex > 0 {
|
|
||||||
previousTradeDate = tradeDates[dateIndex-1]
|
|
||||||
}
|
|
||||||
resp.PreviousTradeDate = previousTradeDate
|
|
||||||
|
|
||||||
previousTwoTradeDate := ""
|
|
||||||
if dateIndex > 1 {
|
|
||||||
previousTwoTradeDate = tradeDates[dateIndex-2]
|
|
||||||
}
|
|
||||||
resp.PreviousTwoTradeDate = previousTwoTradeDate
|
|
||||||
|
|
||||||
stocks, err := s.loadLimitOpenCompareStocks(tradeDate, previousTradeDate, previousTwoTradeDate)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
openLowerStocks := make([]model.LimitOpenCompareStock, 0)
|
|
||||||
otherStocks := make([]model.LimitOpenCompareStock, 0)
|
|
||||||
firstToSecondBoardCount := 0
|
|
||||||
firstSecondBeforeElevenCount := 0
|
|
||||||
for _, stock := range stocks {
|
|
||||||
if stock.IsFirstToSecondBoard {
|
|
||||||
stock.HighlightLabel = "首板进二板"
|
|
||||||
firstToSecondBoardCount++
|
|
||||||
}
|
|
||||||
isOpenLower := previousTradeDate != "" && stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.PreviousClose
|
|
||||||
if isOpenLower && stock.IsFirstToSecondBoard && isBeforeEleven(stock.AkFirstLimitTime) {
|
|
||||||
stock.IsFirstSecondBeforeEleven = true
|
|
||||||
stock.HighlightLabel = "首二一一"
|
|
||||||
firstSecondBeforeElevenCount++
|
|
||||||
}
|
|
||||||
if isOpenLower {
|
|
||||||
openLowerStocks = append(openLowerStocks, stock)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
otherStocks = append(otherStocks, stock)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Summary = model.LimitOpenCompareSummary{
|
|
||||||
LimitCount: len(stocks),
|
|
||||||
OpenLowerPreviousCloseCount: len(openLowerStocks),
|
|
||||||
OthersCount: len(otherStocks),
|
|
||||||
FirstToSecondBoardCount: firstToSecondBoardCount,
|
|
||||||
FirstSecondBeforeElevenCount: firstSecondBeforeElevenCount,
|
|
||||||
}
|
|
||||||
resp.Groups = buildLimitOpenCompareGroups(openLowerStocks, otherStocks)
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LimitBoardService) GetLimitOpenThresholdGroups(tradeDate string) (model.LimitOpenThresholdResponse, error) {
|
|
||||||
resp := model.LimitOpenThresholdResponse{TradeDate: tradeDate}
|
|
||||||
const thresholdRatio = 1.01
|
|
||||||
|
|
||||||
tradeDates, dateIndex, err := s.loadTradeCalendar(tradeDate)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
if dateIndex == -1 {
|
|
||||||
resp.Summary.ThresholdRatio = thresholdRatio
|
|
||||||
resp.Groups = buildLimitOpenThresholdGroups(nil, nil)
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
previousTradeDate := ""
|
|
||||||
if dateIndex > 0 {
|
|
||||||
previousTradeDate = tradeDates[dateIndex-1]
|
|
||||||
}
|
|
||||||
resp.PreviousTradeDate = previousTradeDate
|
|
||||||
|
|
||||||
stocks, err := s.loadLimitOpenThresholdStocks(tradeDate, previousTradeDate)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
withinThresholdStocks := make([]model.LimitOpenThresholdStock, 0)
|
|
||||||
atOrAboveThresholdStocks := make([]model.LimitOpenThresholdStock, 0)
|
|
||||||
for _, stock := range stocks {
|
|
||||||
if stock.HasPreviousClose && stock.PreviousClose > 0 {
|
|
||||||
stock.OpenThresholdPrice = stock.PreviousClose * thresholdRatio
|
|
||||||
}
|
|
||||||
if stock.HasOpen && stock.HasPreviousClose && stock.PreviousClose > 0 {
|
|
||||||
stock.OpenPreviousCloseRatio = stock.Open / stock.PreviousClose
|
|
||||||
}
|
|
||||||
|
|
||||||
if stock.HasOpen && stock.HasPreviousClose && stock.Open < stock.OpenThresholdPrice {
|
|
||||||
withinThresholdStocks = append(withinThresholdStocks, stock)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
atOrAboveThresholdStocks = append(atOrAboveThresholdStocks, stock)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Summary = model.LimitOpenThresholdSummary{
|
|
||||||
LimitCount: len(stocks),
|
|
||||||
OpenWithinThresholdCount: len(withinThresholdStocks),
|
|
||||||
OpenAtOrAboveThresholdCount: len(atOrAboveThresholdStocks),
|
|
||||||
ThresholdRatio: thresholdRatio,
|
|
||||||
}
|
|
||||||
resp.Groups = buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStocks)
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBeforeEleven(limitTime *string) bool {
|
|
||||||
if limitTime == nil || *limitTime == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
timeText := strings.TrimSpace(*limitTime)
|
|
||||||
hour := 0
|
|
||||||
minute := 0
|
|
||||||
second := 0
|
|
||||||
if strings.Contains(timeText, ":") {
|
|
||||||
if n, _ := fmt.Sscanf(timeText, "%d:%d:%d", &hour, &minute, &second); n < 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(timeText) < 4 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch len(timeText) {
|
|
||||||
case 4:
|
|
||||||
timeText += "00"
|
|
||||||
case 5:
|
|
||||||
timeText = "0" + timeText
|
|
||||||
case 6:
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
hour, err = strconv.Atoi(timeText[0:2])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
minute, err = strconv.Atoi(timeText[2:4])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
second, err = strconv.Atoi(timeText[4:6])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return hour < 11
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIndex int) ([]model.LimitBoardDailyMax, error) {
|
|
||||||
startIndex := 0
|
|
||||||
if dateIndex >= 29 {
|
|
||||||
startIndex = dateIndex - 29
|
|
||||||
}
|
|
||||||
windowDates := tradeDates[startIndex : dateIndex+1]
|
|
||||||
if len(windowDates) == 0 {
|
|
||||||
return []model.LimitBoardDailyMax{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var history []limitBoardHistoryRow
|
|
||||||
if err := s.DB.Table(stockLimitCombinedTable).
|
|
||||||
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
|
||||||
Where("trade_date BETWEEN ? AND ? AND final_status = ?", windowDates[0], windowDates[len(windowDates)-1], 1).
|
|
||||||
Order("ts_code ASC, trade_date ASC").
|
|
||||||
Scan(&history).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dateByCode := make(map[string]map[string]struct{})
|
|
||||||
for _, item := range history {
|
|
||||||
if _, ok := dateByCode[item.TsCode]; !ok {
|
|
||||||
dateByCode[item.TsCode] = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
dateByCode[item.TsCode][item.TradeDate] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
maxBoardByDate := make(map[string]int, len(windowDates))
|
|
||||||
for _, tradeDate := range windowDates {
|
|
||||||
maxBoardByDate[tradeDate] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, limitDates := range dateByCode {
|
|
||||||
for idx, tradeDate := range windowDates {
|
|
||||||
if _, ok := limitDates[tradeDate]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
boardCount := 0
|
|
||||||
for j := idx; j >= 0; j-- {
|
|
||||||
if _, ok := limitDates[windowDates[j]]; !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
boardCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if boardCount > maxBoardByDate[tradeDate] {
|
|
||||||
maxBoardByDate[tradeDate] = boardCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := make([]model.LimitBoardDailyMax, 0, len(windowDates))
|
|
||||||
for _, tradeDate := range windowDates {
|
|
||||||
stats = append(stats, model.LimitBoardDailyMax{
|
|
||||||
TradeDate: tradeDate,
|
|
||||||
MaxBoardCount: maxBoardByDate[tradeDate],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int, error) {
|
func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int, error) {
|
||||||
var tradeDates []string
|
var tradeDates []string
|
||||||
if err := s.DB.Table("trade_calendar").
|
if err := s.DB.Table("trade_calendar").
|
||||||
@@ -332,289 +57,3 @@ func (s *LimitBoardService) loadTradeCalendar(tradeDate string) ([]string, int,
|
|||||||
|
|
||||||
return tradeDates, dateIndex, nil
|
return tradeDates, dateIndex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.LimitBoardStock, error) {
|
|
||||||
var stocks []model.LimitBoardStock
|
|
||||||
query := `SELECT
|
|
||||||
l.symbol,
|
|
||||||
l.ts_code,
|
|
||||||
l.name,
|
|
||||||
COALESCE(l.market, '') AS market,
|
|
||||||
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
|
|
||||||
COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS close,
|
|
||||||
COALESCE(l.calc_limit_price, 0) AS limit_price,
|
|
||||||
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
|
|
||||||
l.tushare_amount AS ak_amount,
|
|
||||||
l.tushare_float_mv AS ak_float_market_value,
|
|
||||||
l.tushare_total_mv AS ak_total_market_value,
|
|
||||||
l.tushare_turnover_ratio AS ak_turnover_rate,
|
|
||||||
l.tushare_fd_amount AS ak_seal_amount,
|
|
||||||
l.tushare_first_time AS ak_first_limit_time,
|
|
||||||
l.tushare_last_time AS ak_last_limit_time,
|
|
||||||
l.tushare_open_times AS ak_break_limit_count,
|
|
||||||
l.tushare_up_stat AS ak_limit_up_stat,
|
|
||||||
l.tushare_limit_times AS ak_limit_up_days,
|
|
||||||
l.tushare_industry AS ak_industry,
|
|
||||||
l.calc_vol AS vol
|
|
||||||
FROM stock_limit_combined_data l
|
|
||||||
LEFT JOIN stock_daily d
|
|
||||||
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
|
||||||
WHERE l.trade_date = ? AND l.final_status = 1
|
|
||||||
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
|
||||||
|
|
||||||
err := s.DB.Raw(query, tradeDate).Scan(&stocks).Error
|
|
||||||
return stocks, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeDate, previousTwoTradeDate string) ([]model.LimitOpenCompareStock, error) {
|
|
||||||
var stocks []model.LimitOpenCompareStock
|
|
||||||
query := `SELECT
|
|
||||||
l.symbol,
|
|
||||||
l.ts_code,
|
|
||||||
l.name,
|
|
||||||
COALESCE(l.market, '') AS market,
|
|
||||||
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
|
|
||||||
d.open IS NOT NULL AS has_open,
|
|
||||||
COALESCE(d.open, 0) AS open,
|
|
||||||
COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS close,
|
|
||||||
? AS previous_trade_date,
|
|
||||||
? AS previous_two_trade_date,
|
|
||||||
pd.close IS NOT NULL AS has_previous_close,
|
|
||||||
COALESCE(pd.close, 0) AS previous_close,
|
|
||||||
(? <> '' AND NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM stock_limit_combined_data ppl
|
|
||||||
WHERE ppl.ts_code = l.ts_code AND ppl.trade_date = ? AND ppl.final_status = 1
|
|
||||||
)) AS is_first_to_second_board,
|
|
||||||
COALESCE(l.calc_limit_price, 0) AS limit_price,
|
|
||||||
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
|
|
||||||
l.tushare_amount AS ak_amount,
|
|
||||||
l.tushare_float_mv AS ak_float_market_value,
|
|
||||||
l.tushare_total_mv AS ak_total_market_value,
|
|
||||||
l.tushare_turnover_ratio AS ak_turnover_rate,
|
|
||||||
l.tushare_fd_amount AS ak_seal_amount,
|
|
||||||
l.tushare_first_time AS ak_first_limit_time,
|
|
||||||
l.tushare_last_time AS ak_last_limit_time,
|
|
||||||
l.tushare_open_times AS ak_break_limit_count,
|
|
||||||
l.tushare_up_stat AS ak_limit_up_stat,
|
|
||||||
l.tushare_limit_times AS ak_limit_up_days,
|
|
||||||
l.tushare_industry AS ak_industry,
|
|
||||||
l.calc_vol AS vol
|
|
||||||
FROM stock_limit_combined_data l
|
|
||||||
LEFT JOIN stock_daily d
|
|
||||||
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
|
||||||
LEFT JOIN stock_daily pd
|
|
||||||
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
|
||||||
INNER JOIN stock_limit_combined_data pl
|
|
||||||
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
|
||||||
WHERE l.trade_date = ? AND l.final_status = 1
|
|
||||||
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
|
||||||
|
|
||||||
if previousTradeDate == "" {
|
|
||||||
return stocks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.DB.Raw(query, previousTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTwoTradeDate, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error
|
|
||||||
return stocks, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LimitBoardService) loadLimitOpenThresholdStocks(tradeDate, previousTradeDate string) ([]model.LimitOpenThresholdStock, error) {
|
|
||||||
var stocks []model.LimitOpenThresholdStock
|
|
||||||
query := `SELECT
|
|
||||||
l.symbol,
|
|
||||||
l.ts_code,
|
|
||||||
l.name,
|
|
||||||
COALESCE(l.market, '') AS market,
|
|
||||||
DATE_FORMAT(l.trade_date, '%Y-%m-%d') AS trade_date,
|
|
||||||
d.open IS NOT NULL AS has_open,
|
|
||||||
COALESCE(d.open, 0) AS open,
|
|
||||||
COALESCE(l.calc_close, l.tushare_close, d.close, 0) AS close,
|
|
||||||
? AS previous_trade_date,
|
|
||||||
(pl.calc_close IS NOT NULL OR pl.tushare_close IS NOT NULL OR pd.close IS NOT NULL) AS has_previous_close,
|
|
||||||
COALESCE(pl.calc_close, pl.tushare_close, pd.close, 0) AS previous_close,
|
|
||||||
COALESCE(l.calc_limit_price, 0) AS limit_price,
|
|
||||||
COALESCE(l.tushare_pct_chg, d.pct_chg, 0) AS pct_chg,
|
|
||||||
l.tushare_amount AS ak_amount,
|
|
||||||
l.tushare_float_mv AS ak_float_market_value,
|
|
||||||
l.tushare_total_mv AS ak_total_market_value,
|
|
||||||
l.tushare_turnover_ratio AS ak_turnover_rate,
|
|
||||||
l.tushare_fd_amount AS ak_seal_amount,
|
|
||||||
l.tushare_first_time AS ak_first_limit_time,
|
|
||||||
l.tushare_last_time AS ak_last_limit_time,
|
|
||||||
l.tushare_open_times AS ak_break_limit_count,
|
|
||||||
l.tushare_up_stat AS ak_limit_up_stat,
|
|
||||||
l.tushare_limit_times AS ak_limit_up_days,
|
|
||||||
l.tushare_industry AS ak_industry,
|
|
||||||
l.calc_vol AS vol
|
|
||||||
FROM stock_limit_combined_data l
|
|
||||||
LEFT JOIN stock_daily d
|
|
||||||
ON l.ts_code = d.ts_code AND d.trade_date = l.trade_date
|
|
||||||
LEFT JOIN stock_daily pd
|
|
||||||
ON l.ts_code = pd.ts_code AND pd.trade_date = ?
|
|
||||||
INNER JOIN stock_limit_combined_data pl
|
|
||||||
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.final_status = 1
|
|
||||||
WHERE l.trade_date = ? AND l.final_status = 1
|
|
||||||
ORDER BY d.pct_chg DESC, l.ts_code ASC`
|
|
||||||
|
|
||||||
if previousTradeDate == "" {
|
|
||||||
return stocks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.DB.Raw(query, previousTradeDate, previousTradeDate, previousTradeDate, tradeDate).Scan(&stocks).Error
|
|
||||||
return stocks, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LimitBoardService) loadLimitHistory(stocks []model.LimitBoardStock, tradeDate string) (map[string]map[string]struct{}, error) {
|
|
||||||
codes := make([]string, 0, len(stocks))
|
|
||||||
for _, stock := range stocks {
|
|
||||||
codes = append(codes, stock.TsCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var history []limitBoardHistoryRow
|
|
||||||
if err := s.DB.Table(stockLimitCombinedTable).
|
|
||||||
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
|
||||||
Where("ts_code IN ? AND trade_date <= ? AND final_status = ?", codes, tradeDate, 1).
|
|
||||||
Scan(&history).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
historyMap := make(map[string]map[string]struct{}, len(codes))
|
|
||||||
for _, item := range history {
|
|
||||||
if _, ok := historyMap[item.TsCode]; !ok {
|
|
||||||
historyMap[item.TsCode] = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
historyMap[item.TsCode][item.TradeDate] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return historyMap, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildLimitBoardGroups(stocks []model.LimitBoardStock, tradeDates []string, dateIndex int, historyMap map[string]map[string]struct{}) (map[int][]model.LimitBoardStock, int) {
|
|
||||||
groupMap := make(map[int][]model.LimitBoardStock)
|
|
||||||
maxBoardCount := 0
|
|
||||||
|
|
||||||
for i := range stocks {
|
|
||||||
boardCount := calcBoardCount(tradeDates, dateIndex, historyMap[stocks[i].TsCode])
|
|
||||||
stocks[i].BoardCount = boardCount
|
|
||||||
stocks[i].BoardLabel = boardLabel(boardCount)
|
|
||||||
stocks[i].DisplayStatus = boardLabel(boardCount)
|
|
||||||
|
|
||||||
if boardCount > maxBoardCount {
|
|
||||||
maxBoardCount = boardCount
|
|
||||||
}
|
|
||||||
groupMap[boardCount] = append(groupMap[boardCount], stocks[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return groupMap, maxBoardCount
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcBoardCount(tradeDates []string, dateIndex int, limitDates map[string]struct{}) int {
|
|
||||||
boardCount := 0
|
|
||||||
for i := dateIndex; i >= 0; i-- {
|
|
||||||
if _, ok := limitDates[tradeDates[i]]; !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
boardCount++
|
|
||||||
}
|
|
||||||
return boardCount
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortLimitBoardGroups(groupMap map[int][]model.LimitBoardStock) []model.LimitBoardGroup {
|
|
||||||
boardCounts := make([]int, 0, len(groupMap))
|
|
||||||
for boardCount := range groupMap {
|
|
||||||
boardCounts = append(boardCounts, boardCount)
|
|
||||||
}
|
|
||||||
sort.Sort(sort.Reverse(sort.IntSlice(boardCounts)))
|
|
||||||
|
|
||||||
groups := make([]model.LimitBoardGroup, 0, len(boardCounts))
|
|
||||||
for _, boardCount := range boardCounts {
|
|
||||||
stocks := groupMap[boardCount]
|
|
||||||
sort.Slice(stocks, func(i, j int) bool {
|
|
||||||
if stocks[i].PctChg == stocks[j].PctChg {
|
|
||||||
return stocks[i].TsCode < stocks[j].TsCode
|
|
||||||
}
|
|
||||||
return stocks[i].PctChg > stocks[j].PctChg
|
|
||||||
})
|
|
||||||
|
|
||||||
groups = append(groups, model.LimitBoardGroup{
|
|
||||||
BoardCount: boardCount,
|
|
||||||
Label: boardLabel(boardCount),
|
|
||||||
Count: len(stocks),
|
|
||||||
Stocks: stocks,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildLimitOpenCompareGroups(openLowerStocks, otherStocks []model.LimitOpenCompareStock) []model.LimitOpenCompareGroup {
|
|
||||||
if openLowerStocks == nil {
|
|
||||||
openLowerStocks = []model.LimitOpenCompareStock{}
|
|
||||||
}
|
|
||||||
if otherStocks == nil {
|
|
||||||
otherStocks = []model.LimitOpenCompareStock{}
|
|
||||||
}
|
|
||||||
|
|
||||||
sortLimitOpenCompareStocks(openLowerStocks)
|
|
||||||
sortLimitOpenCompareStocks(otherStocks)
|
|
||||||
|
|
||||||
return []model.LimitOpenCompareGroup{
|
|
||||||
{
|
|
||||||
Key: "open_lower_previous_close",
|
|
||||||
Label: "开盘低于前收",
|
|
||||||
Count: len(openLowerStocks),
|
|
||||||
Stocks: openLowerStocks,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "others",
|
|
||||||
Label: "其他涨停股",
|
|
||||||
Count: len(otherStocks),
|
|
||||||
Stocks: otherStocks,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildLimitOpenThresholdGroups(withinThresholdStocks, atOrAboveThresholdStocks []model.LimitOpenThresholdStock) []model.LimitOpenThresholdGroup {
|
|
||||||
if withinThresholdStocks == nil {
|
|
||||||
withinThresholdStocks = []model.LimitOpenThresholdStock{}
|
|
||||||
}
|
|
||||||
if atOrAboveThresholdStocks == nil {
|
|
||||||
atOrAboveThresholdStocks = []model.LimitOpenThresholdStock{}
|
|
||||||
}
|
|
||||||
|
|
||||||
sortLimitOpenThresholdStocks(withinThresholdStocks)
|
|
||||||
sortLimitOpenThresholdStocks(atOrAboveThresholdStocks)
|
|
||||||
|
|
||||||
return []model.LimitOpenThresholdGroup{
|
|
||||||
{
|
|
||||||
Key: "open_within_previous_close_101",
|
|
||||||
Label: "开盘价低于前收101%",
|
|
||||||
Count: len(withinThresholdStocks),
|
|
||||||
Stocks: withinThresholdStocks,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "open_at_or_above_previous_close_101",
|
|
||||||
Label: "开盘价大于等于前收101%",
|
|
||||||
Count: len(atOrAboveThresholdStocks),
|
|
||||||
Stocks: atOrAboveThresholdStocks,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortLimitOpenCompareStocks(stocks []model.LimitOpenCompareStock) {
|
|
||||||
sort.Slice(stocks, func(i, j int) bool {
|
|
||||||
if stocks[i].PctChg == stocks[j].PctChg {
|
|
||||||
return stocks[i].TsCode < stocks[j].TsCode
|
|
||||||
}
|
|
||||||
return stocks[i].PctChg > stocks[j].PctChg
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortLimitOpenThresholdStocks(stocks []model.LimitOpenThresholdStock) {
|
|
||||||
sort.Slice(stocks, func(i, j int) bool {
|
|
||||||
if stocks[i].PctChg == stocks[j].PctChg {
|
|
||||||
return stocks[i].TsCode < stocks[j].TsCode
|
|
||||||
}
|
|
||||||
return stocks[i].PctChg > stocks[j].PctChg
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user