新增首二一一模型

This commit is contained in:
2026-05-02 11:15:36 +08:00
parent 4462a5978e
commit 8cacfb1c49
6 changed files with 443 additions and 1 deletions
+16
View File
@@ -31,3 +31,19 @@ func (h *LimitBoardHandler) GetOverview(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
func (h *LimitBoardHandler) GetOpenLowerGroups(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.GetLimitOpenCompareGroups(req.TradeDate)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}
+55
View File
@@ -30,6 +30,61 @@ type LimitBoardStock struct {
DisplayStatus string `json:"display_status"`
}
type LimitOpenCompareStock 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"`
PreviousTwoTradeDate string `json:"previous_two_trade_date"`
HasPreviousClose bool `json:"-"`
PreviousClose float64 `json:"previous_close"`
IsFirstToSecondBoard bool `json:"is_first_to_second_board"`
IsFirstSecondBeforeEleven bool `json:"is_first_second_before_eleven"`
HighlightLabel string `json:"highlight_label"`
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"`
Count int `json:"count"`
Stocks []LimitOpenCompareStock `json:"stocks"`
}
type LimitOpenCompareSummary struct {
LimitCount int `json:"limit_count"`
OpenLowerPreviousCloseCount int `json:"open_lower_previous_close_count"`
OthersCount int `json:"others_count"`
FirstToSecondBoardCount int `json:"first_to_second_board_count"`
FirstSecondBeforeElevenCount int `json:"first_second_before_eleven_count"`
}
type LimitOpenCompareResponse struct {
TradeDate string `json:"trade_date"`
PreviousTradeDate string `json:"previous_trade_date"`
PreviousTwoTradeDate string `json:"previous_two_trade_date"`
Summary LimitOpenCompareSummary `json:"summary"`
Groups []LimitOpenCompareGroup `json:"groups"`
}
type LimitBoardGroup struct {
BoardCount int `json:"board_count"`
Label string `json:"label"`
+1
View File
@@ -26,6 +26,7 @@ func NewRouter(commonApi *api.CommonHandler, stockApi *api.StockHandler, limitBo
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)
}
return r
}
+197
View File
@@ -3,6 +3,8 @@ package service
import (
"fmt"
"sort"
"strconv"
"strings"
"gorm.io/gorm"
"level_go/internal/model"
@@ -82,6 +84,113 @@ func (s *LimitBoardService) GetLimitBoardOverview(tradeDate string) (model.Limit
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 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
}
if len(timeText) == 4 {
timeText += "00"
}
if len(timeText) != 6 {
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 {
@@ -199,6 +308,58 @@ func (s *LimitBoardService) loadCurrentLimitStocks(tradeDate string) ([]model.Li
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(d.close, l.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
)) 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
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
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) loadLimitHistory(stocks []model.LimitBoardStock, tradeDate string) (map[string]map[string]struct{}, error) {
codes := make([]string, 0, len(stocks))
for _, stock := range stocks {
@@ -281,3 +442,39 @@ func sortLimitBoardGroups(groupMap map[int][]model.LimitBoardStock) []model.Limi
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 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
})
}