Compare commits
3 Commits
f78dfaec74
...
f1734a68ae
| Author | SHA1 | Date | |
|---|---|---|---|
| f1734a68ae | |||
| cd25b65314 | |||
| 996d4f0af7 |
@@ -4,7 +4,7 @@
|
||||
|
||||
- 服务前缀:`/api/v1`
|
||||
- 响应格式:`application/json`
|
||||
- 当前项目接口总数:5
|
||||
- 当前项目接口总数:7
|
||||
|
||||
## 2. 接口列表
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
| 股票行情 | `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) |
|
||||
| 涨停低开分组 | `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/download/limit-board/second-board-profit-csv` | 下载符合二板早盘开盘阈值条件的次日盈利 CSV 明细 | [download-second-board-profit-csv.md](./download-second-board-profit-csv.md) |
|
||||
|
||||
## 3. 错误响应约定
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# 二板早盘次日盈利 CSV 下载接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
- 接口名称:二板早盘次日盈利 CSV 下载
|
||||
- 请求方式:`GET`
|
||||
- 请求路径:`/api/v1/download/limit-board/second-board-profit-csv`
|
||||
- 接口用途:下载符合二板早盘开盘阈值条件的股票明细,并查看后一交易日是否盈利。
|
||||
|
||||
## 2. 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `start_date` | `string` | 否 | `2020-01-01` | `2026-01-01` | 开始交易日期,格式固定为 `YYYY-MM-DD` |
|
||||
| `end_date` | `string` | 否 | `2026-12-31` | `2026-12-31` | 结束交易日期,格式固定为 `YYYY-MM-DD` |
|
||||
|
||||
- `start_date` 可以等于 `end_date`,用于下载单日数据。
|
||||
- `end_date` 不能早于 `start_date`。
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/download/limit-board/second-board-profit-csv?start_date=2026-01-01&end_date=2026-12-31
|
||||
```
|
||||
|
||||
单日下载示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/download/limit-board/second-board-profit-csv?start_date=2026-04-29&end_date=2026-04-29
|
||||
```
|
||||
|
||||
## 3. 响应说明
|
||||
|
||||
- 响应类型:`text/csv; charset=utf-8`
|
||||
- 响应头:`Content-Disposition: attachment; filename="limit_second_board_profit_YYYYMMDD_YYYYMMDD_daily_detail.csv"`
|
||||
- CSV 文件带 UTF-8 BOM,便于 Excel 直接打开中文列名。
|
||||
|
||||
## 4. CSV 字段
|
||||
|
||||
| 字段名 | 说明 |
|
||||
| --- | --- |
|
||||
| `交易日` | 符合条件的交易日 |
|
||||
| `前一交易日` | 交易日的前一个交易日 |
|
||||
| `前二交易日` | 交易日的前两个交易日 |
|
||||
| `后一交易日` | 用于判断盈利的后一交易日 |
|
||||
| `股票代码` | TS 股票代码 |
|
||||
| `股票简称` | 股票名称 |
|
||||
| `市场` | 市场信息 |
|
||||
| `行业` | 所属行业 |
|
||||
| `T日开盘价` | 交易日开盘价 |
|
||||
| `T日收盘价` | 交易日收盘价 |
|
||||
| `T日涨停价` | 理论涨停价 |
|
||||
| `T-1收盘价` | 前一交易日收盘价 |
|
||||
| `开盘/T-1收盘%` | 交易日开盘价相对前一交易日收盘价的百分比 |
|
||||
| `首次涨停时间` | 交易日首次涨停时间,格式化为 `HH:MM:SS` |
|
||||
| `T+1开盘收益%` | 后一交易日开盘价相对 T 日收盘价的收益率 |
|
||||
| `T+1最高收益%` | 后一交易日最高价相对 T 日收盘价的收益率 |
|
||||
| `T+1收盘收益%` | 后一交易日收盘价相对 T 日收盘价的收益率 |
|
||||
| `T+1收盘是否盈利` | `T+1收盘收益% > 0` 时为 `是`,否则为 `否` |
|
||||
| `T+1盘中是否可盈利` | `T+1最高收益% > 0` 时为 `是`,否则为 `否` |
|
||||
|
||||
## 5. 业务规则
|
||||
|
||||
- T 日涨停。
|
||||
- T-1 交易日也涨停。
|
||||
- T-2 交易日必须不涨停。
|
||||
- T 日开盘价必须低于 T-1 收盘价的 101%。
|
||||
- T 日首次涨停时间必须早于 11:00。
|
||||
- 盈利口径默认按 T 日收盘价买入,T+1 收盘价卖出。
|
||||
@@ -74,7 +74,7 @@ GET /api/v1/limit-board/open-lower-groups?trade_date=2026-04-24
|
||||
| `ak_total_market_value` | `number \| null` | AKShare 总市值 |
|
||||
| `ak_turnover_rate` | `number \| null` | AKShare 换手率 |
|
||||
| `ak_seal_amount` | `number \| null` | AKShare 封板资金 |
|
||||
| `ak_first_limit_time` | `string \| null` | AKShare 首次封板时间 |
|
||||
| `ak_first_limit_time` | `string \| null` | Tushare 首次封板时间,可能为 `09:35:12` 或 `93200` 这类紧凑格式 |
|
||||
| `ak_last_limit_time` | `string \| null` | AKShare 最后封板时间 |
|
||||
| `ak_break_limit_count` | `int \| null` | AKShare 炸板次数 |
|
||||
| `ak_limit_up_stat` | `string \| null` | AKShare 涨停统计 |
|
||||
@@ -145,12 +145,12 @@ GET /api/v1/limit-board/open-lower-groups?trade_date=2026-04-24
|
||||
|
||||
## 5. 业务规则
|
||||
|
||||
- 只统计指定交易日 `stock_limit_data.status = 1` 且前一交易日 `stock_limit_data.status = 1` 的股票。
|
||||
- 只统计指定交易日 `stock_limit_combined_data.final_status = 1` 且前一交易日 `stock_limit_combined_data.final_status = 1` 的股票。
|
||||
- 前一交易日、前二交易日通过 `trade_calendar` 中 `exchange = 'SSE'` 且 `is_open = 1` 的交易日确定。
|
||||
- 只有当日 `stock_daily.open` 和前一交易日 `stock_daily.close` 都存在,且 `open < previous_close` 时,股票才会进入 `open_lower_previous_close` 分组。
|
||||
- 行情缺失、没有前一交易日、或不满足 `open < previous_close` 的股票都会进入 `others` 分组。
|
||||
- 当指定交易日涨停、前一交易日涨停、前二交易日不涨停时,返回 `is_first_to_second_board = true`,并设置 `highlight_label = "首板进二板"`。
|
||||
- 在 `open_lower_previous_close` 分组内,如果同时满足 `is_first_to_second_board = true` 且 `ak_first_limit_time` 早于 `11:00`,返回 `is_first_second_before_eleven = true`,并将 `highlight_label` 设为 `首二一一`。
|
||||
- 在 `open_lower_previous_close` 分组内,如果同时满足 `is_first_to_second_board = true` 且 `ak_first_limit_time` 早于 `11:00`,返回 `is_first_second_before_eleven = true`,并将 `highlight_label` 设为 `首二一一`。`93200` 会按 `09:32:00` 处理。
|
||||
- 每个分组内的 `stocks` 按 `pct_chg` 从大到小排序;如果 `pct_chg` 相同,则按 `ts_code` 升序排序。
|
||||
|
||||
## 6. 错误响应
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# 涨停开盘阈值分组接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
- 接口名称:涨停开盘阈值分组
|
||||
- 请求方式:`GET`
|
||||
- 请求路径:`/api/v1/limit-board/open-threshold`
|
||||
- 接口用途:查询指定交易日涨停且前一交易日也涨停的股票,并按指定交易日开盘价是否低于前一交易日收盘价的 101% 分为两组。
|
||||
|
||||
## 2. 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `trade_date` | `string` | 是 | `2026-04-24` | 交易日,格式固定为 `YYYY-MM-DD` |
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/limit-board/open-threshold?trade_date=2026-04-24
|
||||
```
|
||||
|
||||
## 3. 响应说明
|
||||
|
||||
### 3.1 顶层字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `trade_date` | `string` | 查询的交易日 |
|
||||
| `previous_trade_date` | `string` | 前一交易日;如果查询日没有前一交易日则为空字符串 |
|
||||
| `summary` | `object` | 摘要信息 |
|
||||
| `groups` | `array` | 分组后的股票列表,固定返回两组 |
|
||||
|
||||
### 3.2 summary 字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `limit_count` | `int` | 指定交易日涨停且前一交易日也涨停的股票总数 |
|
||||
| `open_within_threshold_count` | `int` | 指定交易日开盘价低于前一交易日收盘价 101% 的股票数量 |
|
||||
| `open_at_or_above_threshold_count` | `int` | 指定交易日开盘价大于等于前一交易日收盘价 101% 的股票数量 |
|
||||
| `threshold_ratio` | `number` | 分组阈值,固定为 `1.01` |
|
||||
|
||||
### 3.3 groups 字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `key` | `string` | 分组标识,取值为 `open_within_previous_close_101` 或 `open_at_or_above_previous_close_101` |
|
||||
| `label` | `string` | 分组展示文案 |
|
||||
| `count` | `int` | 当前分组内股票数量 |
|
||||
| `stocks` | `array` | 当前分组内的股票列表 |
|
||||
|
||||
### 3.4 groups[].stocks 字段
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `symbol` | `string` | 股票代码 |
|
||||
| `ts_code` | `string` | TS 股票代码 |
|
||||
| `name` | `string` | 股票名称 |
|
||||
| `market` | `string` | 所属板块或市场信息 |
|
||||
| `trade_date` | `string` | 交易日 |
|
||||
| `open` | `number` | 当日开盘价 |
|
||||
| `close` | `number` | 当日收盘价 |
|
||||
| `previous_trade_date` | `string` | 前一交易日 |
|
||||
| `previous_close` | `number` | 前一交易日收盘价,优先取前一交易日 `stock_limit_combined_data.calc_close/tushare_close` |
|
||||
| `open_threshold_price` | `number` | 分组阈值价格,即 `previous_close * 1.01` |
|
||||
| `open_previous_close_ratio` | `number` | 当日开盘价 / 前一交易日收盘价 |
|
||||
| `limit_price` | `number` | 理论涨停价 |
|
||||
| `pct_chg` | `number` | 当日涨跌幅 |
|
||||
| `ak_amount` | `number \| null` | Tushare 成交额 |
|
||||
| `ak_float_market_value` | `number \| null` | Tushare 流通市值 |
|
||||
| `ak_total_market_value` | `number \| null` | Tushare 总市值 |
|
||||
| `ak_turnover_rate` | `number \| null` | Tushare 换手率 |
|
||||
| `ak_seal_amount` | `number \| null` | Tushare 封板资金 |
|
||||
| `ak_first_limit_time` | `string \| null` | Tushare 首次封板时间,可能为 `09:35:12` 或 `93200` 这类紧凑格式 |
|
||||
| `ak_last_limit_time` | `string \| null` | Tushare 最后封板时间 |
|
||||
| `ak_break_limit_count` | `int \| null` | Tushare 炸板次数 |
|
||||
| `ak_limit_up_stat` | `string \| null` | Tushare 涨停统计 |
|
||||
| `ak_limit_up_days` | `int \| null` | Tushare 连板数 |
|
||||
| `ak_industry` | `string \| null` | Tushare 所属行业 |
|
||||
| `vol` | `int \| null` | 成交量 |
|
||||
|
||||
## 4. 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"trade_date": "2026-04-24",
|
||||
"previous_trade_date": "2026-04-23",
|
||||
"summary": {
|
||||
"limit_count": 23,
|
||||
"open_within_threshold_count": 8,
|
||||
"open_at_or_above_threshold_count": 15,
|
||||
"threshold_ratio": 1.01
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"key": "open_within_previous_close_101",
|
||||
"label": "开盘价低于前收101%",
|
||||
"count": 8,
|
||||
"stocks": [
|
||||
{
|
||||
"symbol": "000001",
|
||||
"ts_code": "000001.SZ",
|
||||
"name": "示例股份",
|
||||
"market": "主板",
|
||||
"trade_date": "2026-04-24",
|
||||
"open": 10.09,
|
||||
"close": 11.0,
|
||||
"previous_trade_date": "2026-04-23",
|
||||
"previous_close": 10.0,
|
||||
"open_threshold_price": 10.1,
|
||||
"open_previous_close_ratio": 1.009,
|
||||
"limit_price": 11.0,
|
||||
"pct_chg": 10.0,
|
||||
"ak_amount": 1523000000,
|
||||
"ak_float_market_value": 8245000000,
|
||||
"ak_total_market_value": 12678000000,
|
||||
"ak_turnover_rate": 18.64,
|
||||
"ak_seal_amount": 286000000,
|
||||
"ak_first_limit_time": "93200",
|
||||
"ak_last_limit_time": "144208",
|
||||
"ak_break_limit_count": 2,
|
||||
"ak_limit_up_stat": "2天2板",
|
||||
"ak_limit_up_days": 2,
|
||||
"ak_industry": "软件开发",
|
||||
"vol": 123456789
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "open_at_or_above_previous_close_101",
|
||||
"label": "开盘价大于等于前收101%",
|
||||
"count": 15,
|
||||
"stocks": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 业务规则
|
||||
|
||||
- 只统计指定交易日 `stock_limit_combined_data.final_status = 1` 且前一交易日 `stock_limit_combined_data.final_status = 1` 的股票。
|
||||
- 前一交易日通过 `trade_calendar` 中 `exchange = 'SSE'` 且 `is_open = 1` 的交易日确定。
|
||||
- 分组阈值为 `previous_close * 1.01`。
|
||||
- 当 `open < previous_close * 1.01` 时,股票进入 `open_within_previous_close_101` 分组。
|
||||
- 当 `open >= previous_close * 1.01` 时,股票进入 `open_at_or_above_previous_close_101` 分组。
|
||||
- 如果当日开盘价或前一交易日收盘价缺失,股票进入 `open_at_or_above_previous_close_101` 分组。
|
||||
- 每个分组内的 `stocks` 按 `pct_chg` 从大到小排序;如果 `pct_chg` 相同,则按 `ts_code` 升序排序。
|
||||
|
||||
## 6. 错误响应
|
||||
|
||||
当缺少 `trade_date` 参数时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "trade_date 参数不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
当服务端内部异常时:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "具体错误信息"
|
||||
}
|
||||
```
|
||||
@@ -71,7 +71,7 @@ GET /api/v1/limit-board/overview?trade_date=2026-04-24
|
||||
| `ak_total_market_value` | `number \| null` | AKShare 总市值 |
|
||||
| `ak_turnover_rate` | `number \| null` | AKShare 换手率 |
|
||||
| `ak_seal_amount` | `number \| null` | AKShare 封板资金 |
|
||||
| `ak_first_limit_time` | `string \| null` | AKShare 首次封板时间 |
|
||||
| `ak_first_limit_time` | `string \| null` | Tushare 首次封板时间,可能为 `09:35:12` 或 `93200` 这类紧凑格式 |
|
||||
| `ak_last_limit_time` | `string \| null` | AKShare 最后封板时间 |
|
||||
| `ak_break_limit_count` | `int \| null` | AKShare 炸板次数 |
|
||||
| `ak_limit_up_stat` | `string \| null` | AKShare 涨停统计 |
|
||||
@@ -152,7 +152,7 @@ GET /api/v1/limit-board/overview?trade_date=2026-04-24
|
||||
|
||||
## 6. 业务规则
|
||||
|
||||
- 只统计 `stock_limit_data.status = 1` 的记录,即涨停股票。
|
||||
- 只统计 `stock_limit_combined_data.final_status = 1` 的记录,即涨停股票。
|
||||
- 连板数的计算会结合 `trade_calendar` 开市日历处理,不会被周末或节假日打断。
|
||||
- 首板、二板、三板等分组由后端直接计算,前端不需要二次处理。
|
||||
- `summary.max_board_count` 即当天涨停强度最高的板数。
|
||||
|
||||
@@ -76,7 +76,7 @@ Content-Type: application/json
|
||||
|
||||
## 6. 业务规则
|
||||
|
||||
- 仅统计 `stock_limit_data.status = 1` 的涨停股票。
|
||||
- 仅统计 `stock_limit_combined_data.final_status = 1` 的涨停股票。
|
||||
- `start_date` 用于确定样本池,即当日涨停股票。
|
||||
- `end_date` 用于限定后续表现统计区间。
|
||||
- 如果某只股票在区间内只有涨停当天数据,则不会进入最终结果。
|
||||
|
||||
@@ -47,3 +47,19 @@ func (h *LimitBoardHandler) GetOpenLowerGroups(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) GetOpenThresholdGroups(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.GetLimitOpenThresholdGroups(req.TradeDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSecondProfitStartDate = "2020-01-01"
|
||||
defaultSecondProfitEndDate = "2026-12-31"
|
||||
)
|
||||
|
||||
type secondProfitCSVRow 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"`
|
||||
TsCode string `gorm:"column:ts_code"`
|
||||
Symbol string `gorm:"column:symbol"`
|
||||
Name string `gorm:"column:name"`
|
||||
Market string `gorm:"column:market"`
|
||||
Industry string `gorm:"column:industry"`
|
||||
Open float64 `gorm:"column:open"`
|
||||
Close float64 `gorm:"column:close_price"`
|
||||
LimitPrice float64 `gorm:"column:limit_price"`
|
||||
PreviousClose float64 `gorm:"column:previous_close"`
|
||||
FirstLimitTime sql.NullString `gorm:"column:tushare_first_time"`
|
||||
PctChg sql.NullFloat64 `gorm:"column:pct_chg"`
|
||||
Amount sql.NullFloat64 `gorm:"column:tushare_amount"`
|
||||
TurnoverRate sql.NullFloat64 `gorm:"column:tushare_turnover_ratio"`
|
||||
SealAmount sql.NullFloat64 `gorm:"column:tushare_fd_amount"`
|
||||
BreakLimitCount sql.NullInt64 `gorm:"column:tushare_open_times"`
|
||||
LimitUpStat string `gorm:"column:limit_up_stat"`
|
||||
NextOpen sql.NullFloat64 `gorm:"column:next_open"`
|
||||
NextHigh sql.NullFloat64 `gorm:"column:next_high"`
|
||||
NextLow sql.NullFloat64 `gorm:"column:next_low"`
|
||||
NextClose sql.NullFloat64 `gorm:"column:next_close"`
|
||||
OpenPreviousCloseRatio float64
|
||||
NextOpenReturnPct sql.NullFloat64
|
||||
NextHighReturnPct sql.NullFloat64
|
||||
NextCloseReturnPct sql.NullFloat64
|
||||
NextCloseProfitable string
|
||||
NextIntradayProfitable string
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) DownloadSecondBoardProfitCSV(c *gin.Context) {
|
||||
startDate := c.DefaultQuery("start_date", defaultSecondProfitStartDate)
|
||||
endDate := c.DefaultQuery("end_date", defaultSecondProfitEndDate)
|
||||
if err := validateSecondProfitDateRange(startDate, endDate); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.loadSecondProfitCSVRows(startDate, endDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("limit_second_board_profit_%s_%s_daily_detail.csv",
|
||||
strings.ReplaceAll(startDate, "-", ""),
|
||||
strings.ReplaceAll(endDate, "-", ""),
|
||||
)
|
||||
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(secondProfitCSVHeaders())
|
||||
for _, row := range rows {
|
||||
if err := writer.Write(secondProfitCSVRecord(row)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateSecondProfitDateRange(startDate, endDate string) error {
|
||||
start, err := time.Parse("2006-01-02", startDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start_date 格式必须为 YYYY-MM-DD")
|
||||
}
|
||||
end, err := time.Parse("2006-01-02", endDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("end_date 格式必须为 YYYY-MM-DD")
|
||||
}
|
||||
if end.Before(start) {
|
||||
return fmt.Errorf("end_date 不能早于 start_date")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LimitBoardHandler) loadSecondProfitCSVRows(startDate, endDate string) ([]secondProfitCSVRow, error) {
|
||||
query := `
|
||||
WITH open_calendar AS (
|
||||
SELECT
|
||||
DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date,
|
||||
LAG(DATE_FORMAT(cal_date, '%Y-%m-%d'), 1) OVER (ORDER BY cal_date) AS previous_trade_date,
|
||||
LAG(DATE_FORMAT(cal_date, '%Y-%m-%d'), 2) OVER (ORDER BY cal_date) AS previous_two_trade_date,
|
||||
LEAD(DATE_FORMAT(cal_date, '%Y-%m-%d'), 1) OVER (ORDER BY cal_date) AS next_trade_date
|
||||
FROM trade_calendar
|
||||
WHERE exchange = 'SSE' AND is_open = 1
|
||||
)
|
||||
SELECT
|
||||
c.trade_date,
|
||||
c.previous_trade_date,
|
||||
c.previous_two_trade_date,
|
||||
COALESCE(c.next_trade_date, '') AS next_trade_date,
|
||||
l.ts_code,
|
||||
COALESCE(l.symbol, '') AS symbol,
|
||||
COALESCE(l.name, '') AS name,
|
||||
COALESCE(l.market, '') AS market,
|
||||
COALESCE(l.tushare_industry, '') AS industry,
|
||||
d.open,
|
||||
COALESCE(l.calc_close, l.tushare_close, d.close) AS close_price,
|
||||
COALESCE(l.calc_limit_price, 0) AS limit_price,
|
||||
COALESCE(pl.calc_close, pl.tushare_close, pd.close) AS previous_close,
|
||||
l.tushare_first_time,
|
||||
COALESCE(l.tushare_pct_chg, d.pct_chg) AS pct_chg,
|
||||
l.tushare_amount,
|
||||
l.tushare_turnover_ratio,
|
||||
l.tushare_fd_amount,
|
||||
l.tushare_open_times,
|
||||
COALESCE(l.tushare_up_stat, '') AS limit_up_stat,
|
||||
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 = 1
|
||||
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 = l.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
|
||||
WHERE c.trade_date BETWEEN ? AND ?
|
||||
AND c.previous_trade_date IS NOT NULL
|
||||
AND c.previous_two_trade_date IS NOT NULL
|
||||
AND ppl.ts_code IS NULL
|
||||
AND d.open IS NOT NULL
|
||||
AND COALESCE(pl.calc_close, pl.tushare_close, pd.close) > 0
|
||||
AND d.open < COALESCE(pl.calc_close, pl.tushare_close, pd.close) * 1.01
|
||||
ORDER BY c.trade_date ASC, l.ts_code ASC`
|
||||
|
||||
var rawRows []secondProfitCSVRow
|
||||
if err := h.Service.DB.Raw(query, startDate, endDate).Scan(&rawRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows := make([]secondProfitCSVRow, 0, len(rawRows))
|
||||
for _, row := range rawRows {
|
||||
if !isSecondProfitBeforeEleven(row.FirstLimitTime.String) {
|
||||
continue
|
||||
}
|
||||
row.OpenPreviousCloseRatio = row.Open / row.PreviousClose
|
||||
if row.Close > 0 {
|
||||
row.NextOpenReturnPct = secondProfitPctReturn(row.NextOpen, row.Close)
|
||||
row.NextHighReturnPct = secondProfitPctReturn(row.NextHigh, row.Close)
|
||||
row.NextCloseReturnPct = secondProfitPctReturn(row.NextClose, row.Close)
|
||||
}
|
||||
row.NextCloseProfitable = secondProfitLabel(row.NextCloseReturnPct)
|
||||
row.NextIntradayProfitable = secondProfitLabel(row.NextHighReturnPct)
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func secondProfitCSVHeaders() []string {
|
||||
return []string{
|
||||
"交易日", "前一交易日", "前二交易日", "后一交易日", "股票代码", "股票简称", "市场", "行业",
|
||||
"T日开盘价", "T日收盘价", "T日涨停价", "T-1收盘价", "开盘/T-1收盘%", "首次涨停时间",
|
||||
"T日涨跌幅%", "成交额", "换手率%", "封单额", "开板次数", "涨停统计",
|
||||
"T+1开盘价", "T+1最高价", "T+1最低价", "T+1收盘价",
|
||||
"T+1开盘收益%", "T+1最高收益%", "T+1收盘收益%", "T+1收盘是否盈利", "T+1盘中是否可盈利",
|
||||
}
|
||||
}
|
||||
|
||||
func secondProfitCSVRecord(row secondProfitCSVRow) []string {
|
||||
return []string{
|
||||
row.TradeDate,
|
||||
row.PreviousTradeDate,
|
||||
row.PreviousTwoTradeDate,
|
||||
row.NextTradeDate,
|
||||
row.TsCode,
|
||||
row.Name,
|
||||
row.Market,
|
||||
row.Industry,
|
||||
secondProfitFormatFloat(row.Open),
|
||||
secondProfitFormatFloat(row.Close),
|
||||
secondProfitFormatFloat(row.LimitPrice),
|
||||
secondProfitFormatFloat(row.PreviousClose),
|
||||
secondProfitFormatFloat(secondProfitRound2(row.OpenPreviousCloseRatio * 100)),
|
||||
secondProfitFormatLimitTime(row.FirstLimitTime.String),
|
||||
secondProfitFormatNullFloat(row.PctChg),
|
||||
secondProfitFormatNullFloat(row.Amount),
|
||||
secondProfitFormatNullFloat(row.TurnoverRate),
|
||||
secondProfitFormatNullFloat(row.SealAmount),
|
||||
secondProfitFormatNullInt(row.BreakLimitCount),
|
||||
row.LimitUpStat,
|
||||
secondProfitFormatNullFloat(row.NextOpen),
|
||||
secondProfitFormatNullFloat(row.NextHigh),
|
||||
secondProfitFormatNullFloat(row.NextLow),
|
||||
secondProfitFormatNullFloat(row.NextClose),
|
||||
secondProfitFormatNullFloat(row.NextOpenReturnPct),
|
||||
secondProfitFormatNullFloat(row.NextHighReturnPct),
|
||||
secondProfitFormatNullFloat(row.NextCloseReturnPct),
|
||||
row.NextCloseProfitable,
|
||||
row.NextIntradayProfitable,
|
||||
}
|
||||
}
|
||||
|
||||
func isSecondProfitBeforeEleven(limitTime string) bool {
|
||||
timeText := strings.TrimSpace(limitTime)
|
||||
if timeText == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
hour, minute, second := 0, 0, 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 secondProfitFormatLimitTime(value string) string {
|
||||
timeText := strings.TrimSpace(value)
|
||||
if timeText == "" || 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 secondProfitPctReturn(value sql.NullFloat64, base float64) sql.NullFloat64 {
|
||||
if !value.Valid || base == 0 {
|
||||
return sql.NullFloat64{}
|
||||
}
|
||||
return sql.NullFloat64{Float64: secondProfitRound2((value.Float64 - base) / base * 100), Valid: true}
|
||||
}
|
||||
|
||||
func secondProfitLabel(value sql.NullFloat64) string {
|
||||
if !value.Valid {
|
||||
return "无次日数据"
|
||||
}
|
||||
if value.Float64 > 0 {
|
||||
return "是"
|
||||
}
|
||||
return "否"
|
||||
}
|
||||
|
||||
func secondProfitRound2(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
func secondProfitFormatFloat(value float64) string {
|
||||
return strconv.FormatFloat(value, 'f', -1, 64)
|
||||
}
|
||||
|
||||
func secondProfitFormatNullFloat(value sql.NullFloat64) string {
|
||||
if !value.Valid {
|
||||
return ""
|
||||
}
|
||||
return secondProfitFormatFloat(value.Float64)
|
||||
}
|
||||
|
||||
func secondProfitFormatNullInt(value sql.NullInt64) string {
|
||||
if !value.Valid {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(value.Int64, 10)
|
||||
}
|
||||
@@ -62,6 +62,36 @@ type LimitOpenCompareStock struct {
|
||||
Vol *int64 `json:"vol"`
|
||||
}
|
||||
|
||||
type LimitOpenThresholdStock struct {
|
||||
Symbol string `json:"symbol"`
|
||||
TsCode string `json:"ts_code"`
|
||||
Name string `json:"name"`
|
||||
Market string `json:"market"`
|
||||
TradeDate string `json:"trade_date"`
|
||||
HasOpen bool `json:"-"`
|
||||
Open float64 `json:"open"`
|
||||
Close float64 `json:"close"`
|
||||
PreviousTradeDate string `json:"previous_trade_date"`
|
||||
HasPreviousClose bool `json:"-"`
|
||||
PreviousClose float64 `json:"previous_close"`
|
||||
OpenThresholdPrice float64 `json:"open_threshold_price"`
|
||||
OpenPreviousCloseRatio float64 `json:"open_previous_close_ratio"`
|
||||
LimitPrice float64 `json:"limit_price"`
|
||||
PctChg float64 `json:"pct_chg"`
|
||||
AkAmount *float64 `json:"ak_amount"`
|
||||
AkFloatMarketValue *float64 `json:"ak_float_market_value"`
|
||||
AkTotalMarketValue *float64 `json:"ak_total_market_value"`
|
||||
AkTurnoverRate *float64 `json:"ak_turnover_rate"`
|
||||
AkSealAmount *float64 `json:"ak_seal_amount"`
|
||||
AkFirstLimitTime *string `json:"ak_first_limit_time"`
|
||||
AkLastLimitTime *string `json:"ak_last_limit_time"`
|
||||
AkBreakLimitCount *int `json:"ak_break_limit_count"`
|
||||
AkLimitUpStat *string `json:"ak_limit_up_stat"`
|
||||
AkLimitUpDays *int `json:"ak_limit_up_days"`
|
||||
AkIndustry *string `json:"ak_industry"`
|
||||
Vol *int64 `json:"vol"`
|
||||
}
|
||||
|
||||
type LimitOpenCompareGroup struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
@@ -69,6 +99,13 @@ type LimitOpenCompareGroup struct {
|
||||
Stocks []LimitOpenCompareStock `json:"stocks"`
|
||||
}
|
||||
|
||||
type LimitOpenThresholdGroup struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Count int `json:"count"`
|
||||
Stocks []LimitOpenThresholdStock `json:"stocks"`
|
||||
}
|
||||
|
||||
type LimitOpenCompareSummary struct {
|
||||
LimitCount int `json:"limit_count"`
|
||||
OpenLowerPreviousCloseCount int `json:"open_lower_previous_close_count"`
|
||||
@@ -77,6 +114,13 @@ type LimitOpenCompareSummary struct {
|
||||
FirstSecondBeforeElevenCount int `json:"first_second_before_eleven_count"`
|
||||
}
|
||||
|
||||
type LimitOpenThresholdSummary struct {
|
||||
LimitCount int `json:"limit_count"`
|
||||
OpenWithinThresholdCount int `json:"open_within_threshold_count"`
|
||||
OpenAtOrAboveThresholdCount int `json:"open_at_or_above_threshold_count"`
|
||||
ThresholdRatio float64 `json:"threshold_ratio"`
|
||||
}
|
||||
|
||||
type LimitOpenCompareResponse struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
PreviousTradeDate string `json:"previous_trade_date"`
|
||||
@@ -85,6 +129,13 @@ type LimitOpenCompareResponse struct {
|
||||
Groups []LimitOpenCompareGroup `json:"groups"`
|
||||
}
|
||||
|
||||
type LimitOpenThresholdResponse struct {
|
||||
TradeDate string `json:"trade_date"`
|
||||
PreviousTradeDate string `json:"previous_trade_date"`
|
||||
Summary LimitOpenThresholdSummary `json:"summary"`
|
||||
Groups []LimitOpenThresholdGroup `json:"groups"`
|
||||
}
|
||||
|
||||
type LimitBoardGroup struct {
|
||||
BoardCount int `json:"board_count"`
|
||||
Label string `json:"label"`
|
||||
|
||||
@@ -13,6 +13,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
@@ -25,8 +26,19 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
|
||||
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)
|
||||
v1.GET("/limit-board/open-lower-groups", limitBoardApi.GetOpenLowerGroups)
|
||||
|
||||
limitBoard := v1.Group("/limit-board")
|
||||
{
|
||||
limitBoard.GET("/overview", limitBoardApi.GetOverview)
|
||||
limitBoard.GET("/open-lower-groups", limitBoardApi.GetOpenLowerGroups)
|
||||
limitBoard.GET("/open-threshold", limitBoardApi.GetOpenThresholdGroups)
|
||||
limitBoard.GET("/open-threshold-groups", limitBoardApi.GetOpenThresholdGroups)
|
||||
}
|
||||
|
||||
download := v1.Group("/download")
|
||||
{
|
||||
download.GET("/limit-board/second-board-profit-csv", limitBoardApi.DownloadSecondBoardProfitCSV)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ type LimitBoardService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
const stockLimitCombinedTable = "stock_limit_combined_data"
|
||||
|
||||
type limitBoardHistoryRow struct {
|
||||
TsCode string `gorm:"column:ts_code"`
|
||||
TradeDate string `gorm:"column:trade_date"`
|
||||
@@ -147,6 +149,59 @@ func (s *LimitBoardService) GetLimitOpenCompareGroups(tradeDate string) (model.L
|
||||
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
|
||||
@@ -164,10 +219,13 @@ func isBeforeEleven(limitTime *string) bool {
|
||||
if len(timeText) < 4 {
|
||||
return false
|
||||
}
|
||||
if len(timeText) == 4 {
|
||||
switch len(timeText) {
|
||||
case 4:
|
||||
timeText += "00"
|
||||
}
|
||||
if len(timeText) != 6 {
|
||||
case 5:
|
||||
timeText = "0" + timeText
|
||||
case 6:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
var err error
|
||||
@@ -202,9 +260,9 @@ func (s *LimitBoardService) buildRecentMaxBoardStats(tradeDates []string, dateIn
|
||||
}
|
||||
|
||||
var history []limitBoardHistoryRow
|
||||
if err := s.DB.Table("stock_limit_data").
|
||||
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 status = ?", windowDates[0], windowDates[len(windowDates)-1], 1).
|
||||
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
|
||||
@@ -283,25 +341,25 @@ func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.Li
|
||||
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,
|
||||
l.ak_amount,
|
||||
l.ak_float_market_value,
|
||||
l.ak_total_market_value,
|
||||
l.ak_turnover_rate,
|
||||
l.ak_seal_amount,
|
||||
l.ak_first_limit_time,
|
||||
l.ak_last_limit_time,
|
||||
l.ak_break_limit_count,
|
||||
l.ak_limit_up_stat,
|
||||
l.ak_limit_up_days,
|
||||
l.ak_industry,
|
||||
l.vol
|
||||
FROM stock_limit_data l
|
||||
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 d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
WHERE l.trade_date = ? AND l.status = 1
|
||||
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
|
||||
@@ -318,38 +376,38 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
|
||||
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(d.close, l.close, 0) AS close,
|
||||
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_data ppl
|
||||
WHERE ppl.ts_code = l.ts_code AND ppl.trade_date = ? AND ppl.status = 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.limit_price, 0) AS limit_price,
|
||||
COALESCE(d.pct_chg, 0) AS pct_chg,
|
||||
l.ak_amount,
|
||||
l.ak_float_market_value,
|
||||
l.ak_total_market_value,
|
||||
l.ak_turnover_rate,
|
||||
l.ak_seal_amount,
|
||||
l.ak_first_limit_time,
|
||||
l.ak_last_limit_time,
|
||||
l.ak_break_limit_count,
|
||||
l.ak_limit_up_stat,
|
||||
l.ak_limit_up_days,
|
||||
l.ak_industry,
|
||||
l.vol
|
||||
FROM stock_limit_data l
|
||||
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 d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
LEFT JOIN stock_daily pd
|
||||
ON pd.ts_code = l.ts_code AND pd.trade_date = ?
|
||||
INNER JOIN stock_limit_data pl
|
||||
ON pl.ts_code = l.ts_code AND pl.trade_date = ? AND pl.status = 1
|
||||
WHERE l.trade_date = ? AND l.status = 1
|
||||
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 == "" {
|
||||
@@ -360,6 +418,52 @@ func (s *LimitBoardService) loadLimitOpenCompareStocks(tradeDate, previousTradeD
|
||||
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 d.ts_code = l.ts_code AND d.trade_date = l.trade_date
|
||||
LEFT JOIN stock_daily pd
|
||||
ON pd.ts_code = l.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 {
|
||||
@@ -367,9 +471,9 @@ func (s *LimitBoardService) loadLimitHistory(stocks []model.LimitBoardStock, tra
|
||||
}
|
||||
|
||||
var history []limitBoardHistoryRow
|
||||
if err := s.DB.Table("stock_limit_data").
|
||||
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 status = ?", codes, tradeDate, 1).
|
||||
Where("ts_code IN ? AND trade_date <= ? AND final_status = ?", codes, tradeDate, 1).
|
||||
Scan(&history).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -470,6 +574,33 @@ func buildLimitOpenCompareGroups(openLowerStocks, otherStocks []model.LimitOpenC
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -478,3 +609,12 @@ func sortLimitOpenCompareStocks(stocks []model.LimitOpenCompareStock) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type StockService struct {
|
||||
|
||||
func (s *StockService) GetHistoryKline(code string) ([]model.StockDaily, error) {
|
||||
var dailies []model.StockDaily
|
||||
startDate := "2024-09-24"
|
||||
startDate := "2020-01-02"
|
||||
|
||||
err := s.DB.Table("stock_daily").
|
||||
Where("ts_code = ? AND trade_date >= ?", code, startDate).
|
||||
@@ -38,9 +38,9 @@ func (s *StockService) AnalyzePerformance(startDate, endDate string) ([]model.An
|
||||
}
|
||||
|
||||
// 查询涨停股
|
||||
query := `SELECT l.ts_code, l.name, d.open, d.high FROM stock_limit_data l
|
||||
query := `SELECT l.ts_code, l.name, d.open, COALESCE(l.calc_high, d.high, 0) AS high FROM stock_limit_combined_data l
|
||||
LEFT JOIN stock_daily d ON l.ts_code = d.ts_code AND l.trade_date = d.trade_date
|
||||
WHERE l.trade_date = ? AND l.status = 1`
|
||||
WHERE l.trade_date = ? AND l.final_status = 1`
|
||||
|
||||
if err := s.DB.Raw(query, startDate).Scan(&limits).Error; err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user