新增当日开盘价低于前一交易日涨停价格 101% 的判断。新增下载csv 文件接口。 #3

Merged
yang merged 2 commits from main into master 2026-05-05 19:33:14 +08:00
9 changed files with 278 additions and 57 deletions
Showing only changes of commit 996d4f0af7 - Show all commits
+3 -1
View File
@@ -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. 错误响应约定
+3 -3
View File
@@ -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. 错误响应
+2 -2
View File
@@ -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` 即当天涨停强度最高的板数。
+1 -1
View File
@@ -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` 用于限定后续表现统计区间。
- 如果某只股票在区间内只有涨停当天数据,则不会进入最终结果。
+16
View File
@@ -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)
}
+51
View File
@@ -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"`
+14 -2
View File
@@ -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
}
+185 -45
View File
@@ -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
})
}
+3 -3
View File
@@ -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