package service import ( "fmt" "sort" "strconv" "strings" "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) 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 { 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, 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 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) 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 { 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 } 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 }) }