Compare commits
2 Commits
af318b2604
...
50d1a53944
| Author | SHA1 | Date | |
|---|---|---|---|
| 50d1a53944 | |||
| ee1707c17f |
+3
-1
@@ -24,12 +24,14 @@ func main() {
|
||||
// 3. 依赖注入
|
||||
commonSrv := &service.CommonService{DB: db}
|
||||
stockSrv := &service.StockService{DB: db}
|
||||
limitBoardSrv := &service.LimitBoardService{DB: db}
|
||||
|
||||
commonHandler := &api.CommonHandler{Service: commonSrv}
|
||||
stockHandler := &api.StockHandler{Service: stockSrv}
|
||||
limitBoardHandler := api.NewLimitBoardHandler(limitBoardSrv)
|
||||
|
||||
// 4. 启动路由
|
||||
r := router.NewRouter(commonHandler, stockHandler)
|
||||
r := router.NewRouter(commonHandler, stockHandler, limitBoardHandler)
|
||||
|
||||
port := config.GetServerPort()
|
||||
log.Printf("服务启动于端口 %s", port)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# 接口文档总览
|
||||
|
||||
## 1. 基础说明
|
||||
|
||||
- 服务前缀:`/api/v1`
|
||||
- 响应格式:`application/json`
|
||||
- 当前项目接口总数:4
|
||||
|
||||
## 2. 接口列表
|
||||
|
||||
| 模块 | 方法 | 路径 | 说明 | 文档 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 公共 | `GET` | `/api/v1/common/trade-calendar` | 获取交易日历 | [common-trade-calendar.md](./common-trade-calendar.md) |
|
||||
| 股票分析 | `POST` | `/api/v1/stock/analyze` | 分析指定涨停日股票后续表现 | [stock-analyze.md](./stock-analyze.md) |
|
||||
| 股票行情 | `GET` | `/api/v1/stock/kline/:code` | 获取指定股票历史 K 线 | [stock-kline.md](./stock-kline.md) |
|
||||
| 涨停连板 | `GET` | `/api/v1/limit-board/overview` | 获取交易日涨停分组与近 30 日最高板趋势 | [limit-board-overview.md](./limit-board-overview.md) |
|
||||
|
||||
## 3. 错误响应约定
|
||||
|
||||
当前项目未统一封装错误码,接口错误响应以 `error` 字段返回文本信息。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "参数错误"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 跨域说明
|
||||
|
||||
服务已开启基础跨域头:
|
||||
|
||||
- `Access-Control-Allow-Origin: *`
|
||||
- `Access-Control-Allow-Methods: POST, GET, OPTIONS`
|
||||
- `Access-Control-Allow-Headers: Content-Type`
|
||||
@@ -0,0 +1,56 @@
|
||||
# 交易日历接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
- 接口名称:交易日历
|
||||
- 请求方式:`GET`
|
||||
- 请求路径:`/api/v1/common/trade-calendar`
|
||||
- 接口用途:获取上交所开市交易日列表,并返回当前数据中的最大交易日。
|
||||
|
||||
## 2. 请求参数
|
||||
|
||||
无
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/common/trade-calendar
|
||||
```
|
||||
|
||||
## 3. 响应说明
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `open_dates` | `array[string]` | 全部开市日期列表,格式 `YYYY-MM-DD` |
|
||||
| `max_date` | `string` | 开市日期列表中的最大日期 |
|
||||
|
||||
## 4. 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"open_dates": [
|
||||
"2026-04-20",
|
||||
"2026-04-21",
|
||||
"2026-04-22",
|
||||
"2026-04-23",
|
||||
"2026-04-24"
|
||||
],
|
||||
"max_date": "2026-04-24"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 业务规则
|
||||
|
||||
- 只返回 `trade_calendar` 表中 `exchange = 'SSE'` 且 `is_open = 1` 的交易日。
|
||||
- 日期按升序返回。
|
||||
- `max_date` 等于列表中的最后一个日期。
|
||||
|
||||
## 6. 错误响应
|
||||
|
||||
服务端异常时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "具体错误信息"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,194 @@
|
||||
# 涨停连板概览接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
- 接口名称:涨停连板概览
|
||||
- 请求方式:`GET`
|
||||
- 请求路径:`/api/v1/limit-board/overview`
|
||||
- 接口用途:查询指定交易日的涨停股票,按连板高度分组返回,并附带近 30 个交易日的每日最高连板数。
|
||||
|
||||
## 2. 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `trade_date` | `string` | 是 | `2026-04-24` | 交易日,格式固定为 `YYYY-MM-DD` |
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/limit-board/overview?trade_date=2026-04-24
|
||||
```
|
||||
|
||||
## 3. 响应说明
|
||||
|
||||
### 3.1 顶层字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `trade_date` | `string` | 查询的交易日 |
|
||||
| `summary` | `object` | 当日涨停摘要信息 |
|
||||
| `recent_max_board_stats` | `array` | 近 30 个交易日每天的最高连板数 |
|
||||
| `groups` | `array` | 按连板高度分组后的股票列表,已按板数从高到低排序 |
|
||||
|
||||
### 3.2 summary 字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `max_board_count` | `int` | 当天最高连板数 |
|
||||
| `limit_count` | `int` | 当天涨停股票总数 |
|
||||
| `group_count` | `int` | 当天连板分组数量 |
|
||||
|
||||
### 3.3 recent_max_board_stats 字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `trade_date` | `string` | 交易日 |
|
||||
| `max_board_count` | `int` | 该交易日市场最高连板数 |
|
||||
|
||||
### 3.4 groups 字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `board_count` | `int` | 当前分组对应的连板数 |
|
||||
| `label` | `string` | 当前分组显示文案,如 `首板`、`二板`、`三板` |
|
||||
| `count` | `int` | 当前分组内股票数量 |
|
||||
| `stocks` | `array` | 当前分组内的股票列表 |
|
||||
|
||||
### 3.5 groups[].stocks 字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `symbol` | `string` | 股票代码 |
|
||||
| `ts_code` | `string` | TS 股票代码 |
|
||||
| `name` | `string` | 股票名称 |
|
||||
| `market` | `string` | 所属板块或市场信息 |
|
||||
| `trade_date` | `string` | 交易日 |
|
||||
| `close` | `number` | 收盘价 |
|
||||
| `limit_price` | `number` | 理论涨停价 |
|
||||
| `pct_chg` | `number` | 当日涨跌幅 |
|
||||
| `board_count` | `int` | 该股票对应的连续涨停板数 |
|
||||
| `board_label` | `string` | 连板展示文案,如 `首板`、`二板` |
|
||||
| `display_status` | `string` | 页面可直接使用的状态文案,当前与 `board_label` 一致 |
|
||||
|
||||
## 4. 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"trade_date": "2026-04-24",
|
||||
"summary": {
|
||||
"max_board_count": 4,
|
||||
"limit_count": 23,
|
||||
"group_count": 4
|
||||
},
|
||||
"recent_max_board_stats": [
|
||||
{
|
||||
"trade_date": "2026-03-16",
|
||||
"max_board_count": 2
|
||||
},
|
||||
{
|
||||
"trade_date": "2026-03-17",
|
||||
"max_board_count": 3
|
||||
},
|
||||
{
|
||||
"trade_date": "2026-04-24",
|
||||
"max_board_count": 4
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"board_count": 4,
|
||||
"label": "四板",
|
||||
"count": 1,
|
||||
"stocks": [
|
||||
{
|
||||
"symbol": "000001",
|
||||
"ts_code": "000001.SZ",
|
||||
"name": "示例股份",
|
||||
"market": "主板",
|
||||
"trade_date": "2026-04-24",
|
||||
"close": 12.35,
|
||||
"limit_price": 12.35,
|
||||
"pct_chg": 10.01,
|
||||
"board_count": 4,
|
||||
"board_label": "四板",
|
||||
"display_status": "四板"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 排序规则
|
||||
|
||||
- `groups` 按 `board_count` 从大到小排序。
|
||||
- 每个分组内的 `stocks` 按 `pct_chg` 从大到小排序。
|
||||
- 如果 `pct_chg` 相同,则按 `ts_code` 升序排序。
|
||||
- `recent_max_board_stats` 按交易日升序排序。
|
||||
|
||||
## 6. 业务规则
|
||||
|
||||
- 只统计 `stock_limit_data.status = 1` 的记录,即涨停股票。
|
||||
- 连板数的计算会结合 `trade_calendar` 开市日历处理,不会被周末或节假日打断。
|
||||
- 首板、二板、三板等分组由后端直接计算,前端不需要二次处理。
|
||||
- `summary.max_board_count` 即当天涨停强度最高的板数。
|
||||
- `recent_max_board_stats` 返回截至查询日近 30 个交易日的每日最高连板数。
|
||||
|
||||
## 7. 空数据说明
|
||||
|
||||
当指定交易日没有涨停数据时,返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"trade_date": "2026-04-24",
|
||||
"summary": {
|
||||
"max_board_count": 0,
|
||||
"limit_count": 0,
|
||||
"group_count": 0
|
||||
},
|
||||
"recent_max_board_stats": [
|
||||
{
|
||||
"trade_date": "2026-03-16",
|
||||
"max_board_count": 2
|
||||
}
|
||||
],
|
||||
"groups": []
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 如果 `trade_date` 是有效交易日,即使当天没有涨停数据,仍会返回近 30 个交易日的 `recent_max_board_stats`。
|
||||
- 如果 `trade_date` 本身不是有效交易日,则返回空的 `recent_max_board_stats` 和空的 `groups`。
|
||||
|
||||
## 8. 前端对接建议
|
||||
|
||||
- 页面顶部可直接使用 `summary` 展示摘要卡片:
|
||||
- 当天最高连板数
|
||||
- 当天涨停总数
|
||||
- 当天分组数
|
||||
- 趋势图可直接使用 `recent_max_board_stats`:
|
||||
- X 轴使用 `trade_date`
|
||||
- Y 轴使用 `max_board_count`
|
||||
- 页面主体直接遍历 `groups` 渲染分组区块:
|
||||
- 分组标题使用 `label`
|
||||
- 分组数量使用 `count`
|
||||
- 股票列表使用 `stocks`
|
||||
|
||||
## 9. 错误响应
|
||||
|
||||
当缺少 `trade_date` 参数时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "trade_date 参数不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
当服务端内部异常时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "具体错误信息"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# 涨停表现分析接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
- 接口名称:涨停表现分析
|
||||
- 请求方式:`POST`
|
||||
- 请求路径:`/api/v1/stock/analyze`
|
||||
- 接口用途:基于指定涨停日,分析当日涨停股票在后续区间内的表现。
|
||||
|
||||
## 2. 请求参数
|
||||
|
||||
请求体类型:`application/json`
|
||||
|
||||
| 参数名 | 类型 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `start_date` | `string` | 是 | `2026-04-24` | 涨停发生日,格式 `YYYY-MM-DD` |
|
||||
| `end_date` | `string` | 是 | `2026-05-30` | 统计截止日,格式 `YYYY-MM-DD` |
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
POST /api/v1/stock/analyze
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"start_date": "2026-04-24",
|
||||
"end_date": "2026-05-30"
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 响应说明
|
||||
|
||||
返回值为数组,每个元素代表一只在 `start_date` 涨停的股票。
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `code` | `string` | TS 股票代码 |
|
||||
| `name` | `string` | 股票名称 |
|
||||
| `day_open` | `number` | 涨停当日开盘价 |
|
||||
| `day_high` | `number` | 涨停当日最高价 |
|
||||
| `max_high` | `number` | 逻辑最高点价格 |
|
||||
| `max_date` | `string` | 逻辑最高点对应日期 |
|
||||
| `overall_max_high` | `number` | 统计区间内绝对最高价 |
|
||||
| `overall_max_date` | `string` | 统计区间内绝对最高价对应日期 |
|
||||
| `post_min_low` | `number` | 涨停日之后的最低价 |
|
||||
| `drop_pct` | `number \| null` | 从逻辑高点到最低点的降幅,未跌破时为空 |
|
||||
| `rebound_pct` | `number \| null` | 强势形态下的上涨幅度,或破位后的反弹幅度 |
|
||||
| `close_prices` | `array[number]` | 区间内收盘价序列,用于前端绘图 |
|
||||
|
||||
## 4. 响应示例
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"code": "000001.SZ",
|
||||
"name": "示例股份",
|
||||
"day_open": 10.12,
|
||||
"day_high": 11.13,
|
||||
"max_high": 12.65,
|
||||
"max_date": "2026-04-29",
|
||||
"overall_max_high": 12.88,
|
||||
"overall_max_date": "2026-05-06",
|
||||
"post_min_low": 9.94,
|
||||
"drop_pct": 21.42,
|
||||
"rebound_pct": 8.36,
|
||||
"close_prices": [11.13, 10.88, 10.25, 11.02, 10.77]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 5. 排序规则
|
||||
|
||||
- 返回数组优先按 `drop_pct` 从大到小排序。
|
||||
- `drop_pct` 不为空的股票排在前面。
|
||||
- `drop_pct` 为空的股票排在后面。
|
||||
|
||||
## 6. 业务规则
|
||||
|
||||
- 仅统计 `stock_limit_data.status = 1` 的涨停股票。
|
||||
- `start_date` 用于确定样本池,即当日涨停股票。
|
||||
- `end_date` 用于限定后续表现统计区间。
|
||||
- 如果某只股票在区间内只有涨停当天数据,则不会进入最终结果。
|
||||
|
||||
## 7. 空数据说明
|
||||
|
||||
当指定交易日没有涨停股票,返回空数组:
|
||||
|
||||
```json
|
||||
[]
|
||||
```
|
||||
|
||||
## 8. 错误响应
|
||||
|
||||
当请求体绑定失败时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "参数错误"
|
||||
}
|
||||
```
|
||||
|
||||
当服务端内部异常时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "具体错误信息"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
# 股票历史 K 线接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
- 接口名称:股票历史 K 线
|
||||
- 请求方式:`GET`
|
||||
- 请求路径:`/api/v1/stock/kline/:code`
|
||||
- 接口用途:获取指定股票自 `2024-09-24` 起的历史日线数据。
|
||||
|
||||
## 2. 请求参数
|
||||
|
||||
路径参数:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `code` | `string` | 是 | `000001.SZ` | TS 股票代码 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/stock/kline/000001.SZ
|
||||
```
|
||||
|
||||
## 3. 响应说明
|
||||
|
||||
返回值为数组,每个元素代表一个交易日的 K 线数据。
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `ts_code` | `string` | TS 股票代码 |
|
||||
| `trade_date` | `string` | 交易日时间 |
|
||||
| `open` | `number` | 开盘价 |
|
||||
| `high` | `number` | 最高价 |
|
||||
| `low` | `number` | 最低价 |
|
||||
| `close` | `number` | 收盘价 |
|
||||
| `pct_chg` | `number` | 涨跌幅 |
|
||||
|
||||
## 4. 响应示例
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"ts_code": "000001.SZ",
|
||||
"trade_date": "2026-04-21T00:00:00+08:00",
|
||||
"open": 10.01,
|
||||
"high": 10.55,
|
||||
"low": 9.98,
|
||||
"close": 10.43,
|
||||
"pct_chg": 3.24
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 5. 业务规则
|
||||
|
||||
- 查询起始日期固定为 `2024-09-24`。
|
||||
- 返回结果按 `trade_date` 升序排序。
|
||||
|
||||
## 6. 空数据说明
|
||||
|
||||
若指定股票无数据,返回空数组:
|
||||
|
||||
```json
|
||||
[]
|
||||
```
|
||||
|
||||
## 7. 错误响应
|
||||
|
||||
当路径参数为空时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "股票代码不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
当服务端内部异常时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "具体错误信息"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"level_go/internal/model"
|
||||
"level_go/internal/service"
|
||||
)
|
||||
|
||||
type LimitBoardHandler struct {
|
||||
Service *service.LimitBoardService
|
||||
}
|
||||
|
||||
func NewLimitBoardHandler(s *service.LimitBoardService) *LimitBoardHandler {
|
||||
return &LimitBoardHandler{Service: s}
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) GetOverview(c *gin.Context) {
|
||||
var req model.LimitBoardRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil || req.TradeDate == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "trade_date 参数不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.Service.GetLimitBoardOverview(req.TradeDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
type LimitBoardRequest struct {
|
||||
TradeDate string `form:"trade_date" json:"trade_date"`
|
||||
}
|
||||
|
||||
type LimitBoardStock struct {
|
||||
Symbol string `json:"symbol"`
|
||||
TsCode string `json:"ts_code"`
|
||||
Name string `json:"name"`
|
||||
Market string `json:"market"`
|
||||
TradeDate string `json:"trade_date"`
|
||||
Close float64 `json:"close"`
|
||||
LimitPrice float64 `json:"limit_price"`
|
||||
PctChg float64 `json:"pct_chg"`
|
||||
BoardCount int `json:"board_count"`
|
||||
BoardLabel string `json:"board_label"`
|
||||
DisplayStatus string `json:"display_status"`
|
||||
}
|
||||
|
||||
type LimitBoardGroup struct {
|
||||
BoardCount int `json:"board_count"`
|
||||
Label string `json:"label"`
|
||||
Count int `json:"count"`
|
||||
Stocks []LimitBoardStock `json:"stocks"`
|
||||
}
|
||||
|
||||
type LimitBoardSummary struct {
|
||||
MaxBoardCount int `json:"max_board_count"`
|
||||
LimitCount int `json:"limit_count"`
|
||||
GroupCount int `json:"group_count"`
|
||||
}
|
||||
|
||||
type LimitBoardDailyMax struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
MaxBoardCount int `json:"max_board_count"`
|
||||
}
|
||||
|
||||
type LimitBoardResponse struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
Summary LimitBoardSummary `json:"summary"`
|
||||
RecentMaxBoardStats []LimitBoardDailyMax `json:"recent_max_board_stats"`
|
||||
Groups []LimitBoardGroup `json:"groups"`
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"level_go/internal/api"
|
||||
)
|
||||
|
||||
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler) *gin.Engine {
|
||||
func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBoardApi *api.LimitBoardHandler) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 跨域中间件 (根据需要开启)
|
||||
@@ -25,6 +25,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler) *gin.En
|
||||
v1.GET("/common/trade-calendar", commonApi.GetTradeCalendar)
|
||||
v1.POST("/stock/analyze", stockApi.Analyze)
|
||||
v1.GET("/stock/kline/:code", stockApi.GetKlineData)
|
||||
v1.GET("/limit-board/overview", limitBoardApi.GetOverview)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"level_go/internal/model"
|
||||
)
|
||||
|
||||
type LimitBoardService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
type limitBoardHistoryRow struct {
|
||||
TsCode string `gorm:"column:ts_code"`
|
||||
TradeDate string `gorm:"column:trade_date"`
|
||||
}
|
||||
|
||||
func boardLabel(boardCount int) string {
|
||||
switch boardCount {
|
||||
case 1:
|
||||
return "首板"
|
||||
case 2:
|
||||
return "二板"
|
||||
case 3:
|
||||
return "三板"
|
||||
case 4:
|
||||
return "四板"
|
||||
case 5:
|
||||
return "五板"
|
||||
case 6:
|
||||
return "六板"
|
||||
case 7:
|
||||
return "七板"
|
||||
case 8:
|
||||
return "八板"
|
||||
case 9:
|
||||
return "九板"
|
||||
default:
|
||||
return fmt.Sprintf("%d板", boardCount)
|
||||
}
|
||||
}
|
||||
|
||||
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("stock_limit_data").
|
||||
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
||||
Where("trade_date BETWEEN ? AND ? AND 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) {
|
||||
var tradeDates []string
|
||||
if err := s.DB.Table("trade_calendar").
|
||||
Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date").
|
||||
Where("exchange = ? AND is_open = ? AND cal_date <= ?", "SSE", 1, tradeDate).
|
||||
Order("cal_date ASC").
|
||||
Scan(&tradeDates).Error; err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
|
||||
dateIndex := -1
|
||||
for i, d := range tradeDates {
|
||||
if d == tradeDate {
|
||||
dateIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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(d.close, l.close, 0) AS close,
|
||||
COALESCE(l.limit_price, 0) AS limit_price,
|
||||
COALESCE(d.pct_chg, 0) AS pct_chg
|
||||
FROM stock_limit_data l
|
||||
LEFT JOIN stock_daily d
|
||||
ON d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
WHERE l.trade_date = ? AND l.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("stock_limit_data").
|
||||
Select("ts_code, DATE_FORMAT(trade_date, '%Y-%m-%d') AS trade_date").
|
||||
Where("ts_code IN ? AND trade_date <= ? AND 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
|
||||
}
|
||||
Reference in New Issue
Block a user